Dokumentasi API

Menandatangani Permintaan

Cara membuat tanda tangan HMAC untuk Auth API, langkah demi langkah, dengan contoh curl, Node, Python, PHP, dan Go.

Halaman Dokumentasi

Setiap panggilan Auth API, kecuali ping, ditandatangani dengan secret key. Caranya sama dengan Duo, jadi pustaka resmi Duo bisa langsung dipakai.

Ringkasnya

  1. Ambil waktu sekarang dalam format RFC 2822, misalnya Sun, 27 Sep 2026 08:15:30 -0000. Kirim string yang sama persis di header Date.
  2. Susun string kanonik dari tujuh baris di bawah, dipisah \n, tanpa baris baru di akhir.
  3. Hitung HMAC-SHA512 string itu dengan secret key sebagai kunci, lalu tulis hasilnya dalam heksadesimal huruf kecil.
  4. Kirim header Authorization: Basic base64(ikey + ":" + signature).

String Kanonik

BarisIsiContoh
1Tanggal, sama persis dengan header Date.Sun, 27 Sep 2026 08:15:30 -0000
2Metode HTTP, huruf besar.POST
3Hostname API, huruf kecil, tanpa https:// dan tanpa garis miring.auth.wiracode.com
4Path./auth/v2/preauth
5Parameter 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
6SHA-512 (hex) dari badan permintaan persis seperti dikirim. GET: SHA-512 dari string kosong.cf83e1357eefb8bd…
7SHA-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):

NilaiIsi
ikeyDI9ZQX2M4K7W1T0R5Y8B
skeyq8JH2nV7xTz4LmC9pWd3Rk6YbF1sGe5UaN0oKi2E
DateSun, 27 Sep 2026 08:15:30 -0000
PermintaanPOST https://auth.wiracode.com/auth/v2/preauth
Badan{"username":"rina@example.com"}
String kanonik
Sun, 27 Sep 2026 08:15:30 -0000
POST
auth.wiracode.com
/auth/v2/preauth

2aed91dbfbf8d7ed3d06c220a03720fe91110b7e01e773bfe0ff7f993715fbd07113b57a48430388121beef81f7e6b6a153ab1b0d33625a02121b37bd37ea4ad
cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e
Tanda tangan (HMAC-SHA512, hex)
22280779a61e49291168288a0aa4cf80b4129b0ee01fda17250e80c4a42f655cb5d55df3521f92d8981648997242ee1a7f94b0b85646072b3017dd390087fa86
Permintaan yang dikirim
POST /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

curl + 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.

Node.js (duo.mjs)
// 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", ...}
}
Node.js (check.mjs)
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

Python 3 (standard library)
"""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

Python (duo_client)
"""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 8 (curl)
<?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

Go (standard library)
// 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:

BahasaPustakaContoh
Pythonduo_clientduo_client.Auth(ikey, skey, host="auth.wiracode.com")
Javaduo-clientnew Http.HttpBuilder("POST", "auth.wiracode.com", "/auth/v2/preauth").build()
Node.js@duosecurity/duo_apinew duo_api.Client(ikey, skey, "auth.wiracode.com")
PHPduosecurity/duo_api_phpnew DuoAPI\Auth($ikey, $skey, "auth.wiracode.com")
Goduo_api_golangduoapi.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 tanpa Content-Type: application/json atau application/x-www-form-urlencoded.

Duo adalah merek dagang Cisco. WiraPass tidak berafiliasi dengan Duo maupun Cisco dan hanya menawarkan kompatibilitas API.