- TestDeviceCodes (classes, short): tokens, short-code alphabet and distribution, normalization - TestDeviceAuthURL, TestDeviceDisplayName (short): auth_url template, TTL, PHP's user-agent order - TestDeviceInitiate, TestDeviceStatus, TestDeviceAuthorize: PHP bodies for every state and refusal, bounded cleanup, short-code collision retry, concurrent authorizes binding one user - TestDeviceHandOverConcurrent, TestDeviceListAndRevoke, TestDeviceRevokeAfterRefresh, TestDeviceTokensAtRest - TestDeviceAuthSessionsMigration (updates): columns, constraints, indexes, cascade, rollback and re-up
130 lines
4.0 KiB
Go
130 lines
4.0 KiB
Go
package classes
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestDeviceCodes pins the device token and short code (D-11, D-06)
|
|
// without a database: 10,000 tokens are 64 base64url characters, all
|
|
// distinct, and their stored form is the SHA-256 hex; 10,000 short codes
|
|
// are XXXX-XXXX over DeviceShortCodeAlphabet with every character of the
|
|
// alphabet in every position and no character far from its expected
|
|
// share; NormalizeDeviceShortCode accepts case, spaces and a missing
|
|
// hyphen and refuses lengths other than eight and the excluded letters.
|
|
func TestDeviceCodes(t *testing.T) {
|
|
const draws = 10000
|
|
|
|
t.Run("tokens", func(t *testing.T) {
|
|
seen := make(map[string]struct{}, draws)
|
|
for range draws {
|
|
tok, err := NewDeviceToken()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(tok) != 64 {
|
|
t.Fatalf("token %q has %d characters", tok, len(tok))
|
|
}
|
|
if strings.Trim(tok, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") != "" {
|
|
t.Fatalf("token %q is not base64url", tok)
|
|
}
|
|
if _, dup := seen[tok]; dup {
|
|
t.Fatalf("token %q drawn twice", tok)
|
|
}
|
|
seen[tok] = struct{}{}
|
|
}
|
|
tok, _ := NewDeviceToken()
|
|
sum := sha256.Sum256([]byte(tok))
|
|
if got := HashAPIToken(tok); got != hex.EncodeToString(sum[:]) || len(got) != 64 || got == tok {
|
|
t.Fatalf("stored form %q is not the SHA-256 hex of the token", got)
|
|
}
|
|
})
|
|
|
|
t.Run("short_codes", func(t *testing.T) {
|
|
alphabet := DeviceShortCodeAlphabet
|
|
if alphabet != "23456789ABCDEFGHJKMNPQRSTUVWXYZ" || len(alphabet) != 31 {
|
|
t.Fatalf("alphabet = %q", alphabet)
|
|
}
|
|
format := regexp.MustCompile(`^[` + alphabet + `]{4}-[` + alphabet + `]{4}$`)
|
|
perPosition := make([]map[byte]int, 8)
|
|
for i := range perPosition {
|
|
perPosition[i] = map[byte]int{}
|
|
}
|
|
total := map[byte]int{}
|
|
seen := map[string]struct{}{}
|
|
for range draws {
|
|
code, err := NewDeviceShortCode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !format.MatchString(code) {
|
|
t.Fatalf("short code %q", code)
|
|
}
|
|
seen[code] = struct{}{}
|
|
raw := code[:4] + code[5:]
|
|
for i := 0; i < 8; i++ {
|
|
perPosition[i][raw[i]]++
|
|
total[raw[i]]++
|
|
}
|
|
}
|
|
// 31^8 codes: 10,000 draws repeating more than a handful would mean
|
|
// a broken generator.
|
|
if len(seen) < draws-5 {
|
|
t.Fatalf("%d distinct codes in %d draws", len(seen), draws)
|
|
}
|
|
for i, counts := range perPosition {
|
|
for j := 0; j < len(alphabet); j++ {
|
|
if counts[alphabet[j]] == 0 {
|
|
t.Fatalf("character %q never drawn at position %d", alphabet[j], i)
|
|
}
|
|
}
|
|
}
|
|
// Each character is expected 80000/31 ≈ 2581 times; a modulo bias
|
|
// of the byte draw would push the first 8 characters ~3% up. Allow
|
|
// 10% either way, far outside the noise of a fair draw.
|
|
expected := float64(8*draws) / float64(len(alphabet))
|
|
for j := 0; j < len(alphabet); j++ {
|
|
got := float64(total[alphabet[j]])
|
|
if got < expected*0.9 || got > expected*1.1 {
|
|
t.Fatalf("character %q drawn %v times, expected about %.0f", alphabet[j], got, expected)
|
|
}
|
|
}
|
|
for _, excluded := range "01ILO" {
|
|
if total[byte(excluded)] != 0 {
|
|
t.Fatalf("excluded character %q drawn", excluded)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("normalization", func(t *testing.T) {
|
|
for _, c := range []struct{ in, want string }{
|
|
{"ABCD-EFGH", "ABCD-EFGH"},
|
|
{"abcd-efgh", "ABCD-EFGH"},
|
|
{"AbCd-eFgH", "ABCD-EFGH"},
|
|
{"ABCDEFGH", "ABCD-EFGH"},
|
|
{"abcdefgh", "ABCD-EFGH"},
|
|
{" ab cd ef gh ", "ABCD-EFGH"},
|
|
{"ABCD - EFGH", "ABCD-EFGH"},
|
|
{"A-B-C-D-E-F-G-H", "ABCD-EFGH"},
|
|
{"2345-6789", "2345-6789"},
|
|
{"zzzz zzzz", "ZZZZ-ZZZZ"},
|
|
} {
|
|
if got, ok := NormalizeDeviceShortCode(c.in); !ok || got != c.want {
|
|
t.Errorf("normalize %q = %q, %v; want %q", c.in, got, ok, c.want)
|
|
}
|
|
}
|
|
for _, in := range []string{
|
|
"", "-", "ABCDEFG", "ABCD-EFG", "ABCDEFGHJ", "ABCD-EFGHJ",
|
|
"ABCDEFG0", "ABCDEFG1", "ABCDEFGI", "ABCDEFGL", "ABCDEFGO", "abcdefgo", "abcdefgl", "abcdefgi",
|
|
"ABCD_EFGH", "ABCD.EFGH", "ABCDÉFGH", "ABCD\tEFGH",
|
|
} {
|
|
if got, ok := NormalizeDeviceShortCode(in); ok {
|
|
t.Errorf("normalize %q accepted as %q", in, got)
|
|
}
|
|
}
|
|
})
|
|
}
|