|
| 1 | +'use strict'; |
| 2 | +// Unit test for zero-dep digest-auth implementation |
| 3 | +const should = require('should'); |
| 4 | +const createDigestAuth = require('../lib/digest-auth'); |
| 5 | + |
| 6 | +describe('digest-auth utility', function () { |
| 7 | + it('generates a valid Digest Authorization header (MD5, qop=auth)', function () { |
| 8 | + const user = 'user'; |
| 9 | + const pass = 'password'; |
| 10 | + const challenge = 'Digest realm="[email protected]", qop="auth", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"'; |
| 11 | + |
| 12 | + const auth = createDigestAuth(user, pass, challenge); |
| 13 | + const header = auth.authorize('GET', '/dir/index.html'); |
| 14 | + |
| 15 | + header.should.startWith('Digest '); |
| 16 | + header.should.match(/username="user"/); |
| 17 | + header.should.match(/realm="testrealm@host.com"/); |
| 18 | + header.should.match(/uri="\/dir\/index.html"/); |
| 19 | + header.should.match(/response="[a-f0-9]{32}"/); |
| 20 | + }); |
| 21 | + |
| 22 | + it('handles algorithm="MD5-sess"', function () { |
| 23 | + const challenge = 'Digest realm="edge", algorithm="MD5-sess", qop="auth", nonce="abc123"'; |
| 24 | + const auth = createDigestAuth('alice', 'secret', challenge); |
| 25 | + const header = auth.authorize('GET', '/path'); |
| 26 | + |
| 27 | + header.should.match(/algorithm=MD5-sess/); |
| 28 | + header.should.match(/response="[a-f0-9]{32}"/); |
| 29 | + }); |
| 30 | + |
| 31 | + it('handles qop="auth-int" and body hash', function () { |
| 32 | + const body = 'HELLO'; |
| 33 | + const challenge = 'Digest realm="edge", qop="auth-int", nonce="def456"'; |
| 34 | + const auth = createDigestAuth('bob', 'password', challenge); |
| 35 | + const header = auth.authorize('POST', '/submit', body); |
| 36 | + |
| 37 | + header.should.match(/qop=auth-int/); |
| 38 | + header.should.match(/nc=00000001/); |
| 39 | + header.should.match(/response="[a-f0-9]{32}"/); |
| 40 | + }); |
| 41 | + |
| 42 | + it('increments nonce-count (nc) on successive calls', function () { |
| 43 | + const challenge = 'Digest realm="edge", qop="auth", nonce="ghi789"'; |
| 44 | + const auth = createDigestAuth('carol', 'hunter2', challenge); |
| 45 | + const h1 = auth.authorize('GET', '/a'); |
| 46 | + const h2 = auth.authorize('GET', '/b'); |
| 47 | + |
| 48 | + h1.should.match(/nc=00000001/); |
| 49 | + h2.should.match(/nc=00000002/); |
| 50 | + }); |
| 51 | + |
| 52 | + // RFC 2069 legacy support (no qop) |
| 53 | + it('handles RFC 2069 challenges without qop parameter', function () { |
| 54 | + const challenge = 'Digest realm="legacy", nonce="xyz789"'; |
| 55 | + const auth = createDigestAuth('olduser', 'oldpass', challenge); |
| 56 | + const header = auth.authorize('GET', '/legacy'); |
| 57 | + |
| 58 | + header.should.startWith('Digest '); |
| 59 | + header.should.match(/username="olduser"/); |
| 60 | + header.should.match(/realm="legacy"/); |
| 61 | + header.should.match(/nonce="xyz789"/); |
| 62 | + header.should.match(/response="[a-f0-9]{32}"/); |
| 63 | + // Should NOT contain qop, nc, or cnonce for RFC 2069 |
| 64 | + header.should.not.match(/qop=/); |
| 65 | + header.should.not.match(/nc=/); |
| 66 | + header.should.not.match(/cnonce=/); |
| 67 | + }); |
| 68 | + |
| 69 | + // Multiple qop values |
| 70 | + it('handles multiple qop values and selects auth over auth-int', function () { |
| 71 | + const challenge = 'Digest realm="multi", qop="auth-int,auth", nonce="multi123"'; |
| 72 | + const auth = createDigestAuth('multiuser', 'multipass', challenge); |
| 73 | + const header = auth.authorize('POST', '/multi', 'body'); |
| 74 | + |
| 75 | + header.should.match(/qop=auth/); |
| 76 | + header.should.not.match(/qop=auth-int/); |
| 77 | + }); |
| 78 | + |
| 79 | + it('prefers auth-int when auth is not available', function () { |
| 80 | + const challenge = 'Digest realm="authint", qop="auth-int", nonce="authint123"'; |
| 81 | + const auth = createDigestAuth('intuser', 'intpass', challenge); |
| 82 | + const header = auth.authorize('POST', '/authint', 'testbody'); |
| 83 | + |
| 84 | + header.should.match(/qop=auth-int/); |
| 85 | + }); |
| 86 | + |
| 87 | + // Edge cases and error handling |
| 88 | + it('handles challenges with opaque parameter', function () { |
| 89 | + const challenge = 'Digest realm="opaque-test", qop="auth", nonce="opaque123", opaque="abc123def456"'; |
| 90 | + const auth = createDigestAuth('opaqueuser', 'opaquepass', challenge); |
| 91 | + const header = auth.authorize('GET', '/opaque'); |
| 92 | + |
| 93 | + header.should.match(/opaque="abc123def456"/); |
| 94 | + }); |
| 95 | + |
| 96 | + it('handles challenges with algorithm parameter explicitly set to MD5', function () { |
| 97 | + const challenge = 'Digest realm="explicit", algorithm="MD5", qop="auth", nonce="explicit123"'; |
| 98 | + const auth = createDigestAuth('explicituser', 'explicitpass', challenge); |
| 99 | + const header = auth.authorize('GET', '/explicit'); |
| 100 | + |
| 101 | + header.should.match(/algorithm=MD5/); |
| 102 | + }); |
| 103 | + |
| 104 | + it('handles empty or missing realm gracefully', function () { |
| 105 | + const challenge = 'Digest qop="auth", nonce="norealm123"'; |
| 106 | + const auth = createDigestAuth('noreamluser', 'norealmpass', challenge); |
| 107 | + const header = auth.authorize('GET', '/norealm'); |
| 108 | + |
| 109 | + header.should.match(/realm=""/); |
| 110 | + header.should.match(/response="[a-f0-9]{32}"/); |
| 111 | + }); |
| 112 | + |
| 113 | + it('handles quoted values with embedded commas', function () { |
| 114 | + const challenge = 'Digest realm="test,realm", qop="auth", nonce="comma,test"'; |
| 115 | + const auth = createDigestAuth('commauser', 'commapass', challenge); |
| 116 | + const header = auth.authorize('GET', '/comma'); |
| 117 | + |
| 118 | + header.should.match(/realm="test,realm"/); |
| 119 | + header.should.match(/nonce="comma,test"/); |
| 120 | + }); |
| 121 | + |
| 122 | + // MarkLogic-specific scenarios |
| 123 | + it('works with typical MarkLogic server challenge', function () { |
| 124 | + // Simulate a typical MarkLogic digest challenge |
| 125 | + const challenge = 'Digest realm="public", qop="auth", nonce="1234567890abcdef", opaque="5ccc069c403ebaf9f0171e9517f40e41"'; |
| 126 | + const auth = createDigestAuth('mluser', 'mlpassword', challenge); |
| 127 | + const header = auth.authorize('GET', '/v1/documents'); |
| 128 | + |
| 129 | + header.should.startWith('Digest '); |
| 130 | + header.should.match(/username="mluser"/); |
| 131 | + header.should.match(/realm="public"/); |
| 132 | + header.should.match(/uri="\/v1\/documents"/); |
| 133 | + header.should.match(/qop=auth/); |
| 134 | + header.should.match(/nc=00000001/); |
| 135 | + header.should.match(/cnonce="[a-f0-9]{16}"/); |
| 136 | + header.should.match(/response="[a-f0-9]{32}"/); |
| 137 | + header.should.match(/opaque="5ccc069c403ebaf9f0171e9517f40e41"/); |
| 138 | + }); |
| 139 | + |
| 140 | + it('handles POST requests with body content for MarkLogic document insertion', function () { |
| 141 | + const challenge = 'Digest realm="public", qop="auth-int", nonce="mlnonce123"'; |
| 142 | + const auth = createDigestAuth('mluser', 'mlpass', challenge); |
| 143 | + const jsonBody = '{"test": "document"}'; |
| 144 | + const header = auth.authorize('POST', '/v1/documents', jsonBody); |
| 145 | + |
| 146 | + header.should.match(/qop=auth-int/); |
| 147 | + header.should.match(/uri="\/v1\/documents"/); |
| 148 | + header.should.match(/response="[a-f0-9]{32}"/); |
| 149 | + }); |
| 150 | +}); |
| 151 | + |
0 commit comments