Setiap panggilan Auth API, kecuali ping, ditandatangani dengan secret key. Caranya sama dengan Duo, jadi pustaka resmi Duo bisa langsung dipakai.
Ringkasnya
- Ambil waktu sekarang dalam format RFC 2822, misalnya
Sun, 27 Sep 2026 08:15:30 -0000. Kirim string yang sama persis di headerDate. - Susun string kanonik dari tujuh baris di bawah, dipisah
\n, tanpa baris baru di akhir. - Hitung HMAC-SHA512 string itu dengan secret key sebagai kunci, lalu tulis hasilnya dalam heksadesimal huruf kecil.
- Kirim header
Authorization: Basic base64(ikey + ":" + signature).
String Kanonik
| Baris | Isi | Contoh |
|---|---|---|
| 1 | Tanggal, sama persis dengan header Date. | Sun, 27 Sep 2026 08:15:30 -0000 |
| 2 | Metode HTTP, huruf besar. | POST |
| 3 | Hostname API, huruf kecil, tanpa https:// dan tanpa garis miring. | auth.wiracode.com |
| 4 | Path. | /auth/v2/preauth |
| 5 | Parameter query string, terurut menurut nama dan di-encode (lihat di bawah). Kosong bila tidak ada; untuk POST selalu kosong. | txid=3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b |
| 6 | SHA-512 (hex) dari badan permintaan persis seperti dikirim. GET: SHA-512 dari string kosong. | cf83e1357eefb8bd… |
| 7 | SHA-512 (hex) dari header X-Duo-* yang ikut ditandatangani. Tanpa header itu: SHA-512 dari string kosong. | cf83e1357eefb8bd… |
Encoding Parameter
Nama dan nilai diubah ke UTF-8, lalu setiap byte selain huruf A sampai Z, a sampai z, angka, dan - . _ ~ menjadi %XX dengan huruf besar. Spasi menjadi %20 (bukan +), dan @ menjadi %40. Pasangan nama=nilai diurutkan menurut nama lalu digabung dengan &.
Header X-Duo-*
Header tambahan yang namanya diawali X-Duo- ikut ditandatangani: nama diubah ke huruf kecil dan diurutkan, lalu nama dan nilai digabung dengan byte nol: name1\0value1\0name2\0value2. Kebanyakan integrasi tidak mengirim header ini.
Contoh Lengkap
Dengan nilai berikut (hanya contoh, bukan kunci sungguhan):
| Nilai | Isi |
|---|---|
| ikey | DI9ZQX2M4K7W1T0R5Y8B |
| skey | q8JH2nV7xTz4LmC9pWd3Rk6YbF1sGe5UaN0oKi2E |
| Date | Sun, 27 Sep 2026 08:15:30 -0000 |
| Permintaan | POST https://auth.wiracode.com/auth/v2/preauth |
| Badan | {"username":"rina@example.com"} |
Sun, 27 Sep 2026 08:15:30 -0000
POST
auth.wiracode.com
/auth/v2/preauth
2aed91dbfbf8d7ed3d06c220a03720fe91110b7e01e773bfe0ff7f993715fbd07113b57a48430388121beef81f7e6b6a153ab1b0d33625a02121b37bd37ea4ad
cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e22280779a61e49291168288a0aa4cf80b4129b0ee01fda17250e80c4a42f655cb5d55df3521f92d8981648997242ee1a7f94b0b85646072b3017dd390087fa86POST /auth/v2/preauth HTTP/1.1
Host: auth.wiracode.com
Date: Sun, 27 Sep 2026 08:15:30 -0000
Content-Type: application/json
Authorization: Basic REk5WlFYMk00SzdXMVQwUjVZOEI6MjIyODA3NzlhNjFlNDkyOTExNjgyODhhMGFhNGNmODBiNDEyOWIwZWUwMWZkYTE3MjUwZTgwYzRhNDJmNjU1Y2I1ZDU1ZGYzNTIxZjkyZDg5ODE2NDg5OTcyNDJlZTFhN2Y5NGIwYjg1NjQ2MDcyYjMwMTdkZDM5MDA4N2ZhODY=
{"username":"rina@example.com"}Tanda Tangan Versi 2 dan 4
WiraPass juga menerima tanda tangan versi 2 milik Duo, yang masih dipakai pustaka Go dan Node secara bawaan. Isinya hanya lima baris (tanggal, metode, host, path, parameter). Parameter POST dikirim sebagai form (application/x-www-form-urlencoded) dan ikut di baris kelima, dengan HMAC-SHA512 atau HMAC-SHA1. Versi 4 (tanpa baris ketujuh) juga diterima. Versi 1, yang tidak memuat tanggal, ditolak.
Contoh Kode
Setiap contoh memanggil check lalu preauth dan mencetak jawabannya. Ganti ikey, skey, dan email dengan milik Anda.
Curl dan OpenSSL
#!/usr/bin/env bash
# WiraPass Auth API from the shell: bash, curl and openssl 1.1 or newer.
set -euo pipefail
IKEY="DIXXXXXXXXXXXXXXXXXX"
SKEY="your-secret-key-from-the-operator"
HOST="auth.wiracode.com"
sha512() { openssl dgst -sha512 -r | cut -d' ' -f1; }
# duo_call METHOD PATH [QUERY] [JSON_BODY]
# QUERY must already be canonical: key=value pairs sorted by key and
# encoded with RFC 3986 (a space is %20).
duo_call() {
local method=$1 path=$2 query=${3:-} body=${4:-}
local date canon sig
date=$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S -0000')
canon=$(printf '%s\n%s\n%s\n%s\n%s\n%s\n%s' "$date" "$method" "$HOST" "$path" "$query" \
"$(printf '%s' "$body" | sha512)" "$(printf '' | sha512)")
sig=$(printf '%s' "$canon" | openssl dgst -sha512 -hmac "$SKEY" -r | cut -d' ' -f1)
if [ "$method" = POST ]; then
curl -sS -X POST "https://$HOST$path" -u "$IKEY:$sig" -H "Date: $date" \
-H 'Content-Type: application/json' --data-binary "$body"
else
curl -sS "https://$HOST$path${query:+?$query}" -u "$IKEY:$sig" -H "Date: $date"
fi
echo
}
duo_call GET /auth/v2/check
duo_call POST /auth/v2/preauth '' '{"username":"rina@example.com"}'
Node.js
Simpan fungsinya sebagai duo.mjs, lalu pakai dari file lain.
// WiraPass Auth API from Node.js 18 or newer, no dependencies.
import { createHash, createHmac } from 'node:crypto';
const IKEY = 'DIXXXXXXXXXXXXXXXXXX';
const SKEY = 'your-secret-key-from-the-operator';
const HOST = 'auth.wiracode.com';
// RFC 3986: everything except A-Z a-z 0-9 - . _ ~ is percent-encoded.
const enc = (s) => encodeURIComponent(s).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());
const sha512 = (s) => createHash('sha512').update(s).digest('hex');
export async function duoCall(method, path, params = {}) {
const date = new Date().toUTCString(); // "Sun, 27 Sep 2026 08:15:30 GMT"
const query = method === 'GET'
? Object.keys(params).sort().map((k) => `${enc(k)}=${enc(params[k])}`).join('&')
: '';
const body = method === 'GET' ? '' : JSON.stringify(params);
const canon = [date, method, HOST.toLowerCase(), path, query, sha512(body), sha512('')].join('\n');
const sig = createHmac('sha512', SKEY).update(canon).digest('hex');
const res = await fetch(`https://${HOST}${path}${query ? `?${query}` : ''}`, {
method,
headers: {
Date: date,
Authorization: 'Basic ' + Buffer.from(`${IKEY}:${sig}`).toString('base64'),
...(method === 'GET' ? {} : { 'Content-Type': 'application/json' }),
},
body: method === 'GET' ? undefined : body,
});
return res.json(); // {"stat": "OK", "response": ...} or {"stat": "FAIL", ...}
}
import { duoCall } from './duo.mjs';
console.log(JSON.stringify(await duoCall('GET', '/auth/v2/check')));
console.log(JSON.stringify(await duoCall('POST', '/auth/v2/preauth', { username: 'rina@example.com' })));
Python Tanpa Pustaka
"""WiraPass Auth API from Python 3, standard library only."""
import base64
import email.utils
import hashlib
import hmac
import json
import urllib.error
import urllib.parse
import urllib.request
IKEY = "DIXXXXXXXXXXXXXXXXXX"
SKEY = "your-secret-key-from-the-operator"
HOST = "auth.wiracode.com"
def duo_call(method, path, params=None):
params = params or {}
date = email.utils.formatdate() # "Sun, 27 Sep 2026 08:15:30 -0000"
if method == "GET":
query = "&".join(
"%s=%s" % (urllib.parse.quote(k, "~"), urllib.parse.quote(v, "~"))
for k, v in sorted(params.items()))
body = b""
else:
query = ""
body = json.dumps(params, separators=(",", ":"), sort_keys=True).encode()
canon = "\n".join([date, method, HOST.lower(), path, query,
hashlib.sha512(body).hexdigest(),
hashlib.sha512(b"").hexdigest()])
sig = hmac.new(SKEY.encode(), canon.encode(), hashlib.sha512).hexdigest()
auth = base64.b64encode(("%s:%s" % (IKEY, sig)).encode()).decode()
url = "https://%s%s%s" % (HOST, path, "?" + query if query else "")
req = urllib.request.Request(url, data=body if method != "GET" else None, method=method)
req.add_header("Date", date)
req.add_header("Authorization", "Basic " + auth)
if method != "GET":
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req) as res:
return json.load(res)
except urllib.error.HTTPError as e:
return json.load(e) # {"stat": "FAIL", "code": ..., "message": ...}
print(json.dumps(duo_call("GET", "/auth/v2/check")))
print(json.dumps(duo_call("POST", "/auth/v2/preauth", {"username": "rina@example.com"})))
Python dengan Pustaka Resmi Duo
"""WiraPass with Duo's official Python library: pip install duo_client"""
import duo_client
auth = duo_client.Auth(
ikey="DIXXXXXXXXXXXXXXXXXX",
skey="your-secret-key-from-the-operator",
host="auth.wiracode.com",
)
print(auth.check())
print(auth.preauth(username="rina@example.com"))
PHP
<?php
// WiraPass Auth API from PHP 8 with the curl extension, no libraries.
const IKEY = 'DIXXXXXXXXXXXXXXXXXX';
const SKEY = 'your-secret-key-from-the-operator';
const HOST = 'auth.wiracode.com';
function duo_call(string $method, string $path, array $params = []): array
{
$date = gmdate('D, d M Y H:i:s') . ' -0000';
$query = '';
$body = '';
if ($method === 'GET') {
ksort($params);
$pairs = [];
foreach ($params as $k => $v) {
$pairs[] = rawurlencode($k) . '=' . rawurlencode($v);
}
$query = implode('&', $pairs);
} else {
$body = json_encode((object) $params);
}
$canon = implode("\n", [$date, $method, strtolower(HOST), $path, $query,
hash('sha512', $body), hash('sha512', '')]);
$sig = hash_hmac('sha512', $canon, SKEY);
$headers = ['Date: ' . $date, 'Authorization: Basic ' . base64_encode(IKEY . ':' . $sig)];
$ch = curl_init('https://' . HOST . $path . ($query !== '' ? '?' . $query : ''));
if ($method !== 'GET') {
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
]);
return json_decode(curl_exec($ch), true);
}
echo json_encode(duo_call('GET', '/auth/v2/check')), "\n";
echo json_encode(duo_call('POST', '/auth/v2/preauth', ['username' => 'rina@example.com'])), "\n";
Go
// WiraPass Auth API from Go, standard library only.
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
const (
ikey = "DIXXXXXXXXXXXXXXXXXX"
skey = "your-secret-key-from-the-operator"
host = "auth.wiracode.com"
)
func sha512Hex(b []byte) string {
sum := sha512.Sum512(b)
return hex.EncodeToString(sum[:])
}
// RFC 3986: url.QueryEscape, but a space is %20 and not "+".
func enc(s string) string { return strings.ReplaceAll(url.QueryEscape(s), "+", "%20") }
func duoCall(method, path string, params map[string]string) (string, error) {
date := time.Now().UTC().Format(time.RFC1123Z) // "Sun, 27 Sep 2026 08:15:30 +0000"
query, body := "", []byte{}
if method == http.MethodGet {
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
pairs := make([]string, 0, len(keys))
for _, k := range keys {
pairs = append(pairs, enc(k)+"="+enc(params[k]))
}
query = strings.Join(pairs, "&")
} else {
if params == nil {
params = map[string]string{}
}
body, _ = json.Marshal(params) // keys come out sorted
}
canon := strings.Join([]string{date, method, strings.ToLower(host), path, query, sha512Hex(body), sha512Hex(nil)}, "\n")
mac := hmac.New(sha512.New, []byte(skey))
mac.Write([]byte(canon))
sig := hex.EncodeToString(mac.Sum(nil))
u := "https://" + host + path
if query != "" {
u += "?" + query
}
var reader io.Reader
if method != http.MethodGet {
reader = bytes.NewReader(body)
}
req, err := http.NewRequest(method, u, reader)
if err != nil {
return "", err
}
req.Header.Set("Date", date)
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(ikey+":"+sig)))
if method != http.MethodGet {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
out, err := io.ReadAll(res.Body)
return string(out), err
}
func main() {
for _, c := range []struct {
method, path string
params map[string]string
}{
{"GET", "/auth/v2/check", nil},
{"POST", "/auth/v2/preauth", map[string]string{"username": "rina@example.com"}},
} {
out, err := duoCall(c.method, c.path, c.params)
if err != nil {
panic(err)
}
fmt.Println(out)
}
}
Pustaka Resmi Duo
Pustaka resmi Duo menandatangani sendiri, jadi cukup isi hostname API, ikey, dan skey:
| Bahasa | Pustaka | Contoh |
|---|---|---|
| Python | duo_client | duo_client.Auth(ikey, skey, host="auth.wiracode.com") |
| Java | duo-client | new Http.HttpBuilder("POST", "auth.wiracode.com", "/auth/v2/preauth").build() |
| Node.js | @duosecurity/duo_api | new duo_api.Client(ikey, skey, "auth.wiracode.com") |
| PHP | duosecurity/duo_api_php | new DuoAPI\Auth($ikey, $skey, "auth.wiracode.com") |
| Go | duo_api_golang | duoapi.NewDuoApi(ikey, skey, "auth.wiracode.com", "my-app") |
Bila pustaka menolak sertifikat TLS, baca bagian CA pinning.
Kesalahan yang Sering Terjadi
40103: baris host memakai https://, huruf besar, atau port yang tidak ikut dikirim. Tulis hostname persis seperti di URL, dengan huruf kecil.40103: spasi di-encode sebagai + (gaya form), padahal string kanonik butuh %20.40103: pustaka HTTP menyusun ulang JSON setelah ditandatangani. Tanda tangani byte yang benar-benar dikirim.40104,40105: header Date hilang, bukan format RFC 2822, atau jam server meleset lebih dari 5 menit.40106: POST tanpaContent-Type: application/jsonatauapplication/x-www-form-urlencoded.