├── tools ├── htdigest ├── htpasswd ├── ssha.py ├── openssl.md ├── crypt.md ├── notes.md ├── md5crpyt.py └── md5crpyt.js ├── dist.ini ├── Makefile ├── lib └── resty │ ├── auth.lua │ └── auth │ ├── basic.lua │ └── digest.lua └── README.md /tools/htdigest: -------------------------------------------------------------------------------- 1 | alec:baofeng:0cedab3476e644954945efad54db5dd0 2 | -------------------------------------------------------------------------------- /tools/htpasswd: -------------------------------------------------------------------------------- 1 | apr1:$apr1$oTXgSVb2$BWUVC23txgpWVK518ZHa70 2 | crypt:m7hN3WgIlXVN2 3 | sha:{SHA}2PRZAyDhNDqRW2OUFwZQqPNdaSY= 4 | plain:{PLAIN}plain 5 | ssha:{SSHA}8TJSGo2qOr4GGVoZLtYJDk01YveYVlalZdSneQ== 6 | -------------------------------------------------------------------------------- /tools/ssha.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import base64 3 | import getpass 4 | import hashlib 5 | import os 6 | 7 | salt = os.urandom(8) 8 | 9 | print '{SSHA}' + base64.b64encode( 10 | hashlib.sha1(getpass.getpass() + salt).digest() + salt) 11 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | # distribution config for opm packaging 2 | name = lua-resty-auth 3 | abstract = A Lua resty module for HTTP Authentication (both basic and digest scheme supported, referring to RFC 2617) 4 | author = Hungpu DU (duhoobo) 5 | is_original = yes 6 | license = 2bsd 7 | lib_dir = lib 8 | doc_dir = 9 | exclude_files = 10 | repo_link = https://github.com/duhoobo/lua-resty-auth 11 | main_module = lib/resty/auth.lua 12 | requires = nginx 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | LUA_VERSION=5.1 3 | 4 | LUA_DIR=/usr/local 5 | 6 | # for C modules 7 | LUA_LIBDIR=$(LUA_DIR)/lib/lua/$(LUA_VERSION) 8 | # for Lua modules 9 | LUA_SHAREDIR=$(LUA_DIR)/share/lua/$(LUA_VERSION) 10 | 11 | INSTALL=install 12 | 13 | .PHONY: all 14 | 15 | all: 16 | @echo "Nothing to compile, just 'make install' ..." 17 | 18 | install: 19 | $(INSTALL) -d $(LUA_SHAREDIR)/resty 20 | $(INSTALL) -d $(LUA_SHAREDIR)/resty/auth 21 | $(INSTALL) -m 644 lib/resty/*.lua $(LUA_SHAREDIR)/resty/ 22 | $(INSTALL) -m 644 lib/resty/auth/*.lua $(LUA_SHAREDIR)/resty/auth/ 23 | 24 | -------------------------------------------------------------------------------- /tools/openssl.md: -------------------------------------------------------------------------------- 1 | http://www.networkinghowtos.com/howto/adding-users-to-a-htpasswd-file-for-nginx/ 2 | 3 | 4 | * crypt: 5 | 6 | printf "testuser:$(openssl passwd -crypt Pass123)\n" >> .htpasswd 7 | 8 | * apr1: 9 | 10 | printf "testuser:$(openssl passwd -apr1 Pass123)\n" >> .htpasswd 11 | 12 | * MD5 13 | 14 | printf "testuser:$(openssl passwd -1 Pass123)\n" >> .htpasswd 15 | 16 | * SSHA 17 | 18 | (USERNAME="testuser";PWD="Pass123";SALT="$(openssl rand -base64 3)"; \ 19 | SHA1=$(printf "$PWD$SALT" | openssl dgst -binary -sha1 | \ 20 | sed 's#$#'"$SALT"'#' | base64); \ 21 | printf "$USERNAME:{SSHA}$SHA1\n" >> .htpasswd) 22 | 23 | -------------------------------------------------------------------------------- /lib/resty/auth.lua: -------------------------------------------------------------------------------- 1 | local submodules = { 2 | basic= require("resty.auth.basic"), 3 | digest= require("resty.auth.digest") 4 | } 5 | 6 | 7 | local _M = {} 8 | _M._VERSION = "0.0.1" 9 | 10 | 11 | function _M.setup(args) 12 | if not args.scheme then 13 | return nil, "\"scheme\" needed" 14 | 15 | elseif not submodules[args.scheme] then 16 | return nil, "scheme \"" .. args.scheme .. "\" not supported" 17 | end 18 | 19 | return submodules[args.scheme].setup(args) 20 | end 21 | 22 | 23 | function _M.new(scheme, ...) 24 | if not scheme or not submodules[scheme] then 25 | ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 26 | end 27 | 28 | return submodules[scheme].new(...) 29 | end 30 | 31 | 32 | return _M 33 | -------------------------------------------------------------------------------- /tools/crypt.md: -------------------------------------------------------------------------------- 1 | `crypt()` 2 | ========= 3 | 4 | 5 | [Wikipedia](http://en.wikipedia.org/wiki/Crypt_\(C\)) 6 | 7 | > `crypt` is the library function which is used to compute a password hash that 8 | can be used to store user account passwords while keeping them relatively 9 | secure (a passwd file). The output of the function is not simply the hash - it 10 | is a text string which also encodes the salt (usually the first two characters 11 | are the salt itself and the rest is the hashed result), and identifies the hash 12 | algorithm used. This output string is what is meant for putting in a password 13 | record which may be stored in a plain text file. 14 | > 15 | > Most formally, `crypt` provides cryptographic key derivation functions for 16 | password validation and storage on Unix systems. 17 | > 18 | > The same `crypt` function is used both to generate a new hash for storage and 19 | also to hash a proffered password with a recorded salt for comparison. 20 | > 21 | > Modern Unix implementations of crypt library routine support a variety of 22 | different hash schemes. The particular hash algorithm used can be identified by 23 | a unique code prefix in the resulting hashtext, following a pseudo-standard 24 | called Modular Crypt Format. 25 | > 26 | > Key Derivation Functions Supported by crypt: 27 | > 28 | > * Traditional DES-based scheme 29 | > * BSDi extended DES-based scheme 30 | > * MD5-based scheme (by Poul-Henning Kamp) 31 | > * Blowfish-based scheme 32 | > * NT Hash Scheme 33 | > * SHA2-based scheme 34 | > 35 | > The GNU C Library used by almost all Linux distributions provided an 36 | implmentation of the `crypt` function which supported the DES, MD5 and (since 37 | version 2.7) SHA2 based hashing algorithms methioned above. 38 | 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lua-resty-auth 2 | ============== 3 | 4 | A Lua resty module for HTTP Authentication (both basic and digest scheme 5 | supported, referring to [RFC 2617](http://www.ietf.org/rfc/rfc2617.txt)). 6 | 7 | 8 | 9 | TODO 10 | ---- 11 | 12 | * md5crpyt for scheme __basic__ 13 | * crypt for scheme __basic__ 14 | * test case 15 | * stress test 16 | * security audit 17 | 18 | 19 | 20 | Missing Features 21 | ---------------- 22 | 23 | * qop option `auth-int` 24 | * algorithm `MD5-sess` 25 | 26 | 27 | 28 | Example Usage 29 | ------------- 30 | 31 | lua_shared_dict nonce 2m; 32 | 33 | init_by_lua ' 34 | local auth = require("resty.auth") 35 | 36 | local ok, msg = auth.setup { 37 | scheme= "digest", 38 | shm= "nonce", 39 | user_file= "htdigest", 40 | expires= 10, 41 | replays= 5, 42 | timeout= 10, 43 | } 44 | if not ok then error(msg) end 45 | 46 | local ok, msg = auth.setup { 47 | scheme= "basic", 48 | user_file= "htpasswd" 49 | ) 50 | if not ok then print msg end 51 | '; 52 | 53 | server { 54 | location /auth_basic/ { 55 | access_by_lua ' 56 | local auth = require("resty.auth") 57 | auth.new("basic", "you@site"):auth() 58 | '; 59 | } 60 | 61 | location /auth_digest/ { 62 | access_by_lua ' 63 | local auth = require("resty.auth") 64 | auth.new("digest", "you@site"):auth() 65 | '; 66 | } 67 | } 68 | 69 | 70 | 71 | Thanks 72 | ------ 73 | 74 | 75 | * The idea and some of the code are borrowed from [here](http://www.pppei.net/blog/post/663) 76 | * The module parameters mimic the directives of [ngx_http_auth_digest](http://wiki.nginx.org/HttpAuthDigestModule) 77 | -------------------------------------------------------------------------------- /tools/notes.md: -------------------------------------------------------------------------------- 1 | Excerpts from [A Future-Adaptable Password Scheme](https://www.usenix.org/legacy/events/usenix99/provos/provos.pdf) 2 | 3 | 4 | In the following, we give a brief overview of two password hashing functions in 5 | widespread use today, and state their main differences from bcrypt. 6 | 7 | 8 | 9 | Traditional crypt 10 | ----------------- 11 | 12 | Traditional crypt(3)'s design rationale dates back to 1976 [9]. It uses a 13 | password of up to eight characters as a key for DES [10]. The 56-bit DES key 14 | is formed by combining the low-order 7 bits of each character in the password. 15 | If the password is shorter than 8 characters, it is padded with zero bits on 16 | the right. 17 | 18 | A 12-bit salt is used to perturb the DES algorithm, so that the same password 19 | plaintext can produce 4,096 possible password encryptions. A modification to 20 | the DES algorithm, swapping bits i and i+24 in the DES E-Box output when bit i 21 | is set in the salt, achieves this while also making DES encryption hardware 22 | useless for password guessing. 23 | 24 | The 64-bit constant ``0`` is encrypted 25 times with the DES key. The final 25 | output is the 12-bit salt concatenated with the encrypted 64-bit value. The 26 | resulting 76-bit value is recoded into 13 printable ASCII characters. 27 | 28 | At the time traditional crypt was conceived, it was fast enough for 29 | authentication but too costly for password guessing to be practical. Today, we 30 | are aware that it exhibits three serious limitations: the restricted password 31 | space, the small salt space, and the constant execution cost. In contrast, 32 | bcrypt allows for longer passwords, has salts large enough to be unique over 33 | all time, and has adaptable cost. These limitiations therefore do not apply to 34 | bcrypt. 35 | 36 | 37 | 38 | MD5 crypt 39 | --------- 40 | 41 | 42 | MD5 crypt was written by Poul-Henning Kamp for FreeBSD. The main reason for 43 | using MD5 was to avoid problems with American export prohibitions on 44 | cryptographic products, and to allow for a longer password length than the 8 45 | characters used by DES crypt. The password length is restricted only by MD5's 46 | maximum message size of 264 bits. The salt can vary from 12 to 48 bits. 47 | 48 | MD5 crypt hashes the password and salt in a number of different combinations 49 | to slow down the evaluation speed. Some steps in the algorithm make it 50 | doubtful that the scheme was designed from a cryptographic point of view--for 51 | instance, the binary representation of the password length at some point 52 | determines which data is hashed, for every zero bit the first byte of the 53 | password and for every set bit the first byte of a previous hash computation. 54 | 55 | The output is the concatenation of the version identifier ``$1$``, the salt, 56 | a ``$`` separator, and the 128-bit hash output. 57 | 58 | MD5 crypt places virtually no limit on the size of passwords, while bcrypt has 59 | a maximum of 55 bytes. We do not consider this a serious limitation of bcrypt, 60 | however. Not only are users unlikely to choose such long passwords, but if 61 | they did, MD5 crypt's 128-bit output size would become the limiting factor in 62 | security. A brute force attacker could more easily find short strings hashing 63 | to the same value as a user's password than guess the actual password. 64 | Finally, like DES crypt, MD5 crypt has fixed cost. 65 | 66 | -------------------------------------------------------------------------------- /tools/md5crpyt.py: -------------------------------------------------------------------------------- 1 | # Based on FreeBSD src/lib/libcrypt/crypt.c 1.2 2 | # http://www.freebsd.org/cgi/cvsweb.cgi/~checkout~/src/lib/libcrypt/crypt.c?rev=1.2&content-type=text/plain 3 | 4 | # Original license: 5 | # * "THE BEER-WARE LICENSE" (Revision 42): 6 | # * wrote this file. As long as you retain this notice you 7 | # * can do whatever you want with this stuff. If we meet some day, and you think 8 | # * this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp 9 | 10 | # This port adds no further stipulations. I forfeit any copyright interest. 11 | 12 | import md5 13 | 14 | def md5crypt(password, salt, magic='$1$'): 15 | # /* The password first, since that is what is most unknown */ /* Then our magic string */ /* Then the raw salt */ 16 | m = md5.new() 17 | m.update(password + magic + salt) 18 | 19 | # /* Then just as many characters of the MD5(pw,salt,pw) */ 20 | mixin = md5.md5(password + salt + password).digest() 21 | for i in range(0, len(password)): 22 | m.update(mixin[i % 16]) 23 | 24 | # /* Then something really weird... */ 25 | # Also really broken, as far as I can tell. -m 26 | i = len(password) 27 | while i: 28 | if i & 1: 29 | m.update('\x00') 30 | else: 31 | m.update(password[0]) 32 | i >>= 1 33 | 34 | final = m.digest() 35 | 36 | # /* and now, just to make sure things don't run too fast */ 37 | for i in range(1000): 38 | m2 = md5.md5() 39 | if i & 1: 40 | m2.update(password) 41 | else: 42 | m2.update(final) 43 | 44 | if i % 3: 45 | m2.update(salt) 46 | 47 | if i % 7: 48 | m2.update(password) 49 | 50 | if i & 1: 51 | m2.update(final) 52 | else: 53 | m2.update(password) 54 | 55 | final = m2.digest() 56 | 57 | # This is the bit that uses to64() in the original code. 58 | 59 | itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' 60 | 61 | rearranged = '' 62 | for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)): 63 | v = ord(final[a]) << 16 | ord(final[b]) << 8 | ord(final[c]) 64 | for i in range(4): 65 | rearranged += itoa64[v & 0x3f]; v >>= 6 66 | 67 | v = ord(final[11]) 68 | for i in range(2): 69 | rearranged += itoa64[v & 0x3f]; v >>= 6 70 | 71 | return magic + salt + '$' + rearranged 72 | 73 | if __name__ == '__main__': 74 | 75 | def test(clear_password, the_hash): 76 | magic, salt = the_hash[1:].split('$')[:2] 77 | magic = '$' + magic + '$' 78 | return md5crypt(clear_password, salt, magic) == the_hash 79 | 80 | test_cases = ( 81 | (' ', '$1$yiiZbNIH$YiCsHZjcTkYd31wkgW8JF.'), 82 | ('pass', '$1$YeNsbWdH$wvOF8JdqsoiLix754LTW90'), 83 | ('____fifteen____', '$1$s9lUWACI$Kk1jtIVVdmT01p0z3b/hw1'), 84 | ('____sixteen_____', '$1$dL3xbVZI$kkgqhCanLdxODGq14g/tW1'), 85 | ('____seventeen____', '$1$NaH5na7J$j7y8Iss0hcRbu3kzoJs5V.'), 86 | ('__________thirty-three___________', '$1$HO7Q6vzJ$yGwp2wbL5D7eOVzOmxpsy.'), 87 | ('apache', '$apr1$J.w5a/..$IW9y6DR0oO/ADuhlMF5/X1'), 88 | ('apr1', '$apr1$oTXgSVb2$BWUVC23txgpWVK518ZHa70') 89 | ) 90 | 91 | for clearpw, hashpw in test_cases: 92 | if test(clearpw, hashpw): 93 | print '%s: pass' % clearpw 94 | else: 95 | print '%s: FAIL' % clearpw 96 | -------------------------------------------------------------------------------- /tools/md5crpyt.js: -------------------------------------------------------------------------------- 1 | /* From https://github.com/BlaM/cryptMD5-for-javascript/blob/master/cryptmd5.js 2 | */ 3 | 4 | /*- 5 | * Copyright (c) 2003 Poul-Henning Kamp 6 | * All rights reserved. 7 | * 8 | * Converted to JavaScript / node.js and modified by Dominik Deobald / Interdose.com 9 | * 10 | * Redistribution and use in source and binary forms, with or without 11 | * modification, are permitted provided that the following conditions 12 | * are met: 13 | * 1. Redistributions of source code must retain the above copyright 14 | * notice, this list of conditions and the following disclaimer. 15 | * 2. Redistributions in binary form must reproduce the above copyright 16 | * notice, this list of conditions and the following disclaimer in the 17 | * documentation and/or other materials provided with the distribution. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 23 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 25 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 26 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 28 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 29 | * SUCH DAMAGE. 30 | */ 31 | var crypto = require('crypto'); 32 | // http://code.activestate.com/recipes/325204-passwd-file-compatible-1-md5-crypt/ 33 | // http://www.freebsd.org/cgi/cvsweb.cgi/~checkout~/src/lib/libcrypt/crypt.c?rev=1.2&content-type=text/plain 34 | // http://www.freebsd.org/cgi/cvsweb.cgi/~checkout~/src/lib/libcrypt/crypt-md5.c 35 | exports.cryptMD5 = function(pw, salt) { 36 | var magic = '$1$'; 37 | var fin; 38 | var sp = salt || generateSalt(8); 39 | var ctx = crypto.createHash('md5'); 40 | // The password first, since that is what is most unknown 41 | // Then our magic string 42 | // Then the raw salt 43 | ctx.update(pw + magic + sp); 44 | // Then just as many characters of the MD5(pw,sp,pw) 45 | var ctx1 = crypto.createHash('md5'); 46 | ctx1.update(pw); 47 | ctx1.update(sp); 48 | ctx1.update(pw); 49 | var fin = ctx1.digest(); 50 | for(var i = 0; i < pw.length ; i++) { 51 | ctx.update(fin.substr(i % 16, 1)); 52 | } 53 | // Then something really weird... 54 | // Also really broken, as far as I can tell. -m 55 | // Agreed ;) -dd 56 | for (var i = pw.length; i; i >>= 1) { 57 | ctx.update ( (i & 1) ? "\x00" : pw[0] ); 58 | } 59 | fin = ctx.digest(); 60 | // and now, just to make sure things don't run too fast 61 | for (var i = 0; i < 1000; i++) { 62 | var ctx1 = crypto.createHash('md5'); 63 | if (i & 1) { 64 | ctx1.update(pw); 65 | } else { 66 | ctx1.update(fin); 67 | } 68 | if (i % 3) { 69 | ctx1.update(sp); 70 | } 71 | if (i % 7) { 72 | ctx1.update(pw); 73 | } 74 | if (i & 1) { 75 | ctx1.update(fin); 76 | } else { 77 | ctx1.update(pw); 78 | } 79 | fin = ctx1.digest() 80 | } 81 | return magic + sp + '$' + to64(fin); 82 | } 83 | function to64(data) { 84 | // This is the bit that uses to64() in the original code. 85 | var itoa64 = ['.','/','0','1','2','3','4','5','6','7','8','9','A','B','C','D', 86 | 'E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T', 87 | 'U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','i','j', 88 | 'k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z']; 89 | var rearranged = ''; 90 | var opt = [[0, 6, 12], [1, 7, 13], [2, 8, 14], [3, 9, 15], [4, 10, 5]]; 91 | for (var p in opt) { 92 | var l = data.charCodeAt( opt[p][0] ) << 16 | data.charCodeAt( opt[p][1] ) << 8 | data.charCodeAt( opt[p][2] ); 93 | for (var i = 0; i < 4; i++) { 94 | rearranged += itoa64[l & 0x3f]; l >>= 6 95 | } 96 | } 97 | var l = data.charCodeAt(11); 98 | for (var i = 0; i < 2; i++) { 99 | rearranged += itoa64[l & 0x3f]; l >>= 6 100 | } 101 | return rearranged; 102 | } 103 | var SaltLength = 8; 104 | function generateSalt(len) { 105 | var set = '0123456789abcdefghijklmnopqurstuvwxyzABCDEFGHIJKLMNOPQURSTUVWXYZ', 106 | setLen = set.length, 107 | salt = ''; 108 | for (var i = 0; i < len; i++) { 109 | var p = Math.floor(Math.random() * setLen); 110 | salt += set[p]; 111 | } 112 | return salt; 113 | } 114 | -------------------------------------------------------------------------------- /lib/resty/auth/basic.lua: -------------------------------------------------------------------------------- 1 | 2 | local tab_concat = table.concat 3 | local str_find = string.find 4 | local str_sub = string.sub 5 | local str_gsub = string.gsub 6 | 7 | 8 | local _M = { 9 | credentials= {}, 10 | default_realm= "Default Realm", 11 | } 12 | 13 | 14 | -- 15 | -- See http://nginx.org/en/docs/http/ngx_http_auth_basic_module.html#auth_basic_user_file 16 | -- for more information 17 | local function parse_line(line) 18 | local states = { 19 | sw_start= 0, 20 | sw_user= 1, 21 | sw_before_method= 2, 22 | sw_method= 3, 23 | sw_salt= 4, 24 | sw_cipher= 5, 25 | sw_done= 6, 26 | } 27 | 28 | local user_start, user_end, method_start, method_end 29 | local salt_start, salt_end, cipher_start, cipher_end 30 | local user, method, salt, cipher 31 | 32 | local state, sep = states.sw_start, "" 33 | 34 | for i = 1, #line, 1 do 35 | local c = line:sub(i, i) 36 | 37 | if state == states.sw_start then 38 | if c == "#" then -- skip comment 39 | return nil, "a comment line" 40 | elseif c == ":" then -- empty user 41 | return nil, "empty user" 42 | elseif c ~= " " and c ~= "\t" then 43 | state, user_start = states.sw_user, i 44 | end 45 | 46 | elseif state == states.sw_user then 47 | if c == ":" then 48 | state, user_end = states.sw_before_method, i-1 49 | end 50 | 51 | elseif state == states.sw_before_method then 52 | if c == "$" or c == "{" then 53 | state, method_start, sep = states.sw_method, i+1, c 54 | else 55 | state, method, cipher_start = states.sw_cipher, "crypt", i 56 | end 57 | 58 | elseif state == states.sw_method then 59 | if c == "$" and sep == "$" then 60 | state, method_end, salt_start = states.sw_salt, i-1, i+1 61 | elseif c == "}" and sep == "{" then 62 | state, method_end, cipher_start = states.sw_cipher, i-1, i+1 63 | end 64 | 65 | elseif state == states.sw_salt then 66 | if c == "$" then 67 | state, salt_end, cipher_start = states.sw_cipher, i-1, i+1 68 | end 69 | 70 | elseif state == states.sw_cipher then 71 | if c == ":" then 72 | state, cipher_end = states.sw_done, i-1 73 | elseif i == #line then 74 | state, cipher_end = states.sw_done, i 75 | end 76 | 77 | elseif state == states.sw_done then 78 | break 79 | end 80 | end 81 | 82 | if state ~= states.sw_done then 83 | return nil, "invalid format" 84 | end 85 | 86 | -- user 87 | user = line:sub(user_start, user_end) 88 | 89 | -- method 90 | if method_start then 91 | method = line:sub(method_start, method_end) 92 | end 93 | method = method:lower() 94 | 95 | -- cipher 96 | cipher = line:sub(cipher_start, cipher_end) 97 | 98 | -- salt and final cipher 99 | if method == "apr1" then 100 | if not salt_start then 101 | return nil, "\"apr1\" should carry plain salt" 102 | end 103 | salt = line:sub(salt_start, salt_end) 104 | 105 | return user, method, salt, cipher 106 | 107 | elseif method == "plain" then 108 | return user, method, "", cipher 109 | 110 | elseif method == "sha" then 111 | -- the {SHA} method shouldn't be used for security reasons as it's 112 | -- vulnerable to attackes using rainbow tables. Use either {SSHA} or 113 | -- {MD5} instead if you care about compatibility with other platforms, 114 | -- or `crypt()` schemes provided by your OS if you aren't 115 | cipher = ngx.decode_base64(cipher) 116 | if not cipher then 117 | return nil, "sha cipher invalid" 118 | end 119 | return user, method, "", cipher 120 | 121 | elseif method == "crypt" then 122 | if #cipher ~= 13 then 123 | return nil, "crypt cipher invalid" 124 | end 125 | return user, method, cipher:sub(1, 2), cipher:sub(3) 126 | 127 | elseif method == "ssha" then 128 | -- a {SSH} password is just a {SSHA} one with empty salt 129 | local bin = ngx.decode_base64(cipher) 130 | if not bin or #bin < 20 then 131 | return nil, "{SSHA} cipher invalid" 132 | end 133 | 134 | return user, method, bin:sub(21), bin:sub(1, 20) 135 | 136 | else 137 | return nil, "encrpytion method not support" 138 | end 139 | end 140 | 141 | 142 | local function validate_plain(passwd, salt, cipher) 143 | return (passwd == cipher) 144 | end 145 | 146 | 147 | local function validate_sha(passwd, salt, cipher) 148 | return (ngx.sha1_bin(passwd) == cipher) 149 | end 150 | 151 | 152 | local function validate_ssha(passwd, salt, cipher) 153 | return (ngx.sha1_bin(passwd .. salt) == cipher) 154 | end 155 | 156 | 157 | local function validate_apr1(passwd, salt, cipher) 158 | -- Apache's apr1 crypt is Poul-Henning Kamp's MD5 crypt 159 | -- algorithm with $apr1$ magic. 160 | return false 161 | end 162 | 163 | 164 | local function validate_crypt(passwd, salt, cipher) 165 | -- 166 | return false 167 | end 168 | 169 | 170 | local validators = { 171 | plain= validate_plain, 172 | sha=validate_sha, 173 | ssha= validate_ssha, 174 | apr1= validate_apr1, 175 | crypt= validate_crypt, 176 | } 177 | 178 | local function validate(credentials, user, passwd) 179 | if not credentials[user] then 180 | return false 181 | end 182 | 183 | local cred = credentials[user] 184 | return validators[cred.method](passwd, cred.salt, cred.cipher) 185 | end 186 | 187 | 188 | local function challenge(realm) 189 | ngx.header.www_authenticate = tab_concat { 190 | "Basic realm=\"", realm, "\""} 191 | 192 | return ngx.exit(ngx.HTTP_UNAUTHORIZED) 193 | end 194 | 195 | 196 | function _M.setup(args) 197 | if not args.user_file then 198 | return nil, "\"user_file\" needed" 199 | end 200 | 201 | local user, method, salt, cipher 202 | 203 | local file, err = io.open(args.user_file, "r") 204 | if not file then 205 | return nil, err 206 | end 207 | 208 | local users = 0 209 | for line in file:lines() do 210 | user, method, salt, cipher = parse_line(line) 211 | 212 | if user then 213 | _M.credentials[user] = {method= method, salt= salt, cipher= cipher} 214 | users = users + 1 215 | else 216 | print("[" .. line .. "] error: " .. method) 217 | end 218 | end 219 | 220 | file:close() 221 | 222 | if users == 0 then 223 | return false, "\"user_file\" no valid lines" 224 | end 225 | 226 | return true 227 | end 228 | 229 | 230 | function _M.auth(self, realm) 231 | realm = realm or _M.default_realm 232 | 233 | -- credentials 234 | local header = ngx.var.http_authorization 235 | if not header then 236 | return challenge(realm) 237 | end 238 | 239 | local prefix = "Basic " 240 | if str_sub(header, 1, #prefix) ~= prefix then 241 | return challenge(realm) 242 | end 243 | 244 | -- base64-user-pass 245 | local b64_userpass = str_gsub(str_sub(header, #prefix+1), " ", "") 246 | 247 | -- user-pass 248 | local userpass = ngx.decode_base64(b64_userpass) 249 | if not userpass then 250 | return challenge(realm) 251 | end 252 | 253 | local colon = str_find(userpass, ":") 254 | if not colon then 255 | return challenge(realm) 256 | end 257 | 258 | local user = str_sub(userpass, 1, colon - 1) 259 | local passwd = str_sub(userpass, colon + 1) 260 | 261 | if not validate(self.credentials, user, passwd) then 262 | return challenge(realm) 263 | end 264 | 265 | return ngx.exit(ngx.OK) 266 | end 267 | 268 | 269 | function _M.new() 270 | return setmetatable({}, {__index= _M}) 271 | end 272 | 273 | 274 | return _M 275 | -------------------------------------------------------------------------------- /lib/resty/auth/digest.lua: -------------------------------------------------------------------------------- 1 | local math_seed = math.randomseed 2 | local math_rand = math.random 3 | local tab_concat = table.concat 4 | 5 | 6 | local _M = { 7 | shm= nil, 8 | expires= nil, 9 | replays= nil, 10 | timeout= nil, 11 | salt= nil, 12 | credentials= {}, 13 | } 14 | 15 | 16 | local function get_value(header, name, quoted) 17 | local n = header:find("[, ]" .. name .. "=") 18 | if not n then return nil end 19 | 20 | n = n + 1 + #name + 1 -- 1 for ',' or ' ', 1 for '=' 21 | 22 | local states = {sw_start= 0, sw_value= 2, sw_done= 3} 23 | local value_start, value_end 24 | local state = states.sw_start 25 | 26 | for i = n, #header, 1 do 27 | local c = string.sub(header, i, i) 28 | 29 | if state == states.sw_start then 30 | if quoted then 31 | if c == "\"" then 32 | state, value_start = states.sw_value, i+1 33 | else 34 | return nil 35 | end 36 | else 37 | state, value_start = states.sw_value, i 38 | end 39 | 40 | elseif state == states.sw_value then 41 | if quoted then 42 | if c == "\"" then 43 | state, value_end = states.sw_done, i-1 44 | end 45 | elseif c == "," then 46 | state, value_end = states.sw_done, i-1 47 | elseif i == #header then 48 | state, value_end = states.sw_done, i 49 | end 50 | end 51 | end 52 | if state ~= states.sw_done then 53 | return nil 54 | end 55 | 56 | return header:sub(value_start, value_end) 57 | end 58 | 59 | 60 | local function get_context(header) 61 | local prefix = "Digest " 62 | if header:sub(1, #prefix) ~= prefix then 63 | return nil 64 | end 65 | 66 | local ctx = {} 67 | 68 | ctx.user = get_value(header, "username", true) 69 | ctx.qop = get_value(header, "qop", false) 70 | ctx.realm = get_value(header, "realm", true) 71 | ctx.nonce = get_value(header, "nonce", true) 72 | ctx.nc = get_value(header, "nc", false) 73 | ctx.uri = get_value(header, "uri", true) 74 | ctx.cnonce = get_value(header, "cnonce", true) 75 | ctx.response = get_value(header, "response", true) 76 | ctx.opaque = get_value(header, "opaque", true) 77 | 78 | -- `opaque` is optional 79 | if not ctx.user or not ctx.response or not ctx.uri or 80 | not ctx.nonce or not ctx.realm 81 | then 82 | return nil 83 | end 84 | 85 | -- if qop exsits, "auth" is the only allowed value for it 86 | if ctx.qop and (ctx.qop ~= "auth" or not ctx.cnonce or not ctx.nc) 87 | then 88 | return nil 89 | end 90 | 91 | return ctx 92 | end 93 | 94 | 95 | local function parse_line(line) 96 | local states = { 97 | sw_start= 0, 98 | sw_user= 1, 99 | sw_realm= 2, 100 | sw_cipher= 3, 101 | sw_done = 4, 102 | } 103 | 104 | local user_start, user_end, realm_start, realm_end 105 | local cipher_start, cipher_end 106 | local user, realm, cipher 107 | 108 | local state = states.sw_start 109 | 110 | for i = 1, #line, 1 do 111 | local c = line:sub(i, i) 112 | 113 | if state == states.sw_start then 114 | if c == "#" then -- skip comment 115 | return nil, "a comment line" 116 | elseif c == ":" then 117 | return nil, "empty user" 118 | elseif c ~= " " and c ~= "\t" then 119 | state, user_start = states.sw_user, i 120 | end 121 | 122 | elseif state == states.sw_user then 123 | if c == ":" then 124 | state, user_end, realm_start = states.sw_realm, i-1, i+1 125 | end 126 | 127 | elseif state == states.sw_realm then 128 | if c == ":" then 129 | state, realm_end, cipher_start = states.sw_cipher, i-1, i+1 130 | end 131 | 132 | elseif state == states.sw_cipher then 133 | if c == ":" then 134 | state, cipher_end = states.sw_done, i-1 135 | elseif i == #line then 136 | state, cipher_end = states.sw_done, i 137 | end 138 | 139 | elseif state == states.sw_done then 140 | break 141 | end 142 | end 143 | 144 | if state ~= states.sw_done then 145 | return nil, "invalid format" 146 | end 147 | 148 | user = line:sub(user_start, user_end) 149 | realm = line:sub(realm_start, realm_end) 150 | cipher = line:sub(cipher_start, cipher_end) 151 | 152 | return user, realm, cipher 153 | end 154 | 155 | 156 | local function next_nonce(shared_dict, salt, timeout, expires) 157 | local ok, gc, err, now, forcible, nonce 158 | 159 | gc, err = shared_dict:incr("global_counter", 1) 160 | now = ngx.time() 161 | 162 | nonce = ngx.encode_base64(ngx.hmac_sha1(salt, now .. ":" .. gc)) 163 | 164 | ok, err, forcible = shared_dict:set(nonce, 0, now + timeout, now + expires) 165 | if not ok then 166 | return nil, err 167 | end 168 | 169 | return nonce 170 | end 171 | 172 | 173 | -- nonce stale or not 174 | local function nonce_stale(shared_dict, nonce, replays) 175 | local nc, err = shared_dict:incr(nonce, 1) 176 | if not nc or nc > replays then -- already evicted or overused 177 | return true 178 | end 179 | 180 | local val, expires_at = shared_dict:get(nonce) 181 | if not val then -- already evicted 182 | return true 183 | end 184 | 185 | if expires_at <= ngx.now() then 186 | shared_dict:delete(nonce) 187 | return true 188 | end 189 | 190 | -- not expires and not overused 191 | return false 192 | end 193 | 194 | 195 | -- shm, user_file, expires, replays, timeout 196 | function _M.setup(args) 197 | if not args.shm then 198 | return nil, "\"shm\" needed" 199 | end 200 | 201 | if not ngx.shared[args.shm] then 202 | return nil, "shm \"" .. args.shm .. "\" not exists" 203 | end 204 | 205 | _M.shm = args.shm 206 | 207 | if not args.user_file then 208 | return nil, "\"user_file\" needed" 209 | end 210 | 211 | local user, realm, cipher 212 | 213 | local file, err = io.open(args.user_file, "r") 214 | if not file then 215 | return nil, err 216 | end 217 | 218 | local users = 0 219 | for line in file:lines() do 220 | user, realm, cipher = parse_line(line) 221 | 222 | if user then 223 | _M.credentials[user] = {realm= realm, cipher= cipher} 224 | print(user, realm, cipher) 225 | users = users + 1 226 | else 227 | print("[" .. line .. "] invalid: " .. realm) 228 | end 229 | end 230 | 231 | file:close() 232 | 233 | if users == 0 then 234 | return false, "\"user_file\" no valid lines" 235 | end 236 | 237 | -- Once a digest challenge has been successfully answered by the client, 238 | -- subsequent requests will attempt to re-use the 'nonce' value from the 239 | -- original challenge. To complicate MitM attacks, it's best to limit the 240 | -- duration a cached nonce will be accepted. 241 | _M.expires = args.expires or 10 242 | -- Nonce re-use should also be limited to a fixed number of requests. 243 | _M.replays = args.replays or 20 244 | -- When a client first requests a protected page, the server returns a 401 245 | -- status code along with a challenge in the __WWW-Authenticate__ header. 246 | -- At this point most browsers will present a dialog box to the user 247 | -- prompting them to log in. `timeout` defines how long challenges will 248 | -- remain valid. If the user waits longer than this time before submitting 249 | -- their name and password, the challenge will be considered `stale` and 250 | -- they will be prompted to log in again. 251 | _M.timeout = args.timeout or 60 252 | 253 | -- initialize nonce counter 254 | math_seed(ngx.time()) 255 | ngx.shared[args.shm]:set("global_counter", math_rand(1, 10000000)) 256 | 257 | -- generate a random salt to encrypt nonce 258 | local salt, chars = "", "0123456789,.abcdefghijklmnopqrstuvwxyz-=_+!" .. 259 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 260 | for i = 1, 8, 1 do 261 | local n = math_rand(#chars) 262 | salt = salt .. chars:sub(n, n) 263 | end 264 | _M.salt = salt 265 | 266 | return true 267 | end 268 | 269 | 270 | function _M.challenge(self, stale) 271 | local nonce = next_nonce(ngx.shared[self.shm], self.salt, self.timeout, 272 | self.expires) 273 | if not nonce then 274 | return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 275 | end 276 | 277 | ngx.header.www_authenticate = tab_concat { 278 | "Digest", 279 | " realm=\"", self.realm or "", "\"", 280 | " domain=\"", self.domain or "", "\"", 281 | " nonce=\"", nonce, "\"", 282 | " stale=", stale and "true" or "false", 283 | " algorithm=MD5", 284 | " qop=\"auth\"" 285 | } 286 | 287 | return ngx.exit(ngx.HTTP_UNAUTHORIZED) 288 | end 289 | 290 | 291 | -- @return pass or not, stale or not 292 | function _M.verify(self, ctx) 293 | local cred = self.credentials[ctx.user] 294 | if not cred or cred.realm ~= ctx.realm then 295 | -- no such user or realm mismatch 296 | return false, false 297 | end 298 | 299 | -- verification for "request-digest" 300 | -- 301 | local ha1, ha2 = cred.cipher, ngx.md5(tab_concat({ngx.req.get_method(), 302 | ctx.uri}, ":")) 303 | local digest 304 | 305 | if ctx.qop then 306 | digest = ngx.md5(tab_concat({ha1, ctx.nonce, ctx.nc, ctx.cnonce, 307 | ctx.qop, ha2}, ":")) 308 | else 309 | digest = ngx.md5(tab_concat({ha1, ctx.nonce, ha2}, ":")) 310 | end 311 | 312 | if digest ~= ctx.response then 313 | -- RFC 2617: If the request-digest is invalid, then a login failure 314 | -- should be logged, since repeated login failures from a single client 315 | -- may indicate an attacker attempting to guess passwords 316 | print("client from " .. ngx.var.remote_addr .. " request-digest error") 317 | return false, false 318 | end 319 | 320 | -- verification for "nonce" 321 | -- 322 | local stale = nonce_stale(ngx.shared[self.shm], ctx.nonce, self.replays) 323 | if stale then 324 | return false, true 325 | end 326 | 327 | return true 328 | end 329 | 330 | 331 | function _M.auth(self) 332 | local header = ngx.var.http_authorization 333 | if not header then 334 | return self:challenge(false) 335 | end 336 | 337 | local ctx = get_context(header) 338 | if not ctx then 339 | return ngx.exit(ngx.HTTP_BAD_REQUEST) -- suggestion of RFC 2617 340 | end 341 | 342 | local pass, stale = self:verify(ctx) 343 | if not pass then 344 | return self:challenge(stale) 345 | end 346 | 347 | return ngx.exit(ngx.OK) 348 | end 349 | 350 | 351 | function _M.new(realm, domain) 352 | return setmetatable({realm= realm, domain= domain}, {__index= _M}) 353 | end 354 | 355 | 356 | return _M 357 | --------------------------------------------------------------------------------