-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathprivacyidea.lua
More file actions
187 lines (155 loc) · 5.25 KB
/
privacyidea.lua
File metadata and controls
187 lines (155 loc) · 5.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
-- -- Copyright (C) by Daniel Hoffend (dhoffend)
-- ----------------------------------
-- extract authentication credentials
-- ----------------------------------
function extract_credentials()
-- Test Authentication header is set and with a value
local header = ngx.req.get_headers()['Authorization']
if header == nil or header:find(" ") == nil then
return false, false
end
local divider = header:find(' ')
if header:sub(0, divider-1) ~= 'Basic' then
return false, false
end
local auth = ngx.decode_base64(header:sub(divider+1))
if auth == nil or auth:find(':') == nil then
return false, false
end
local divider = auth:find(':')
return auth:sub(0, divider-1), auth:sub(divider+1)
end
-- ------------------------
-- Connect to redis storage
-- ------------------------
function redis_connect(host, port)
local redis = require "nginx/redis"
local red = redis:new()
red:set_timeout(1000) -- 1 sec
local ok, err = red:connect(host, port)
if not ok then
ngx.log(ngx.ERR, "failed to connect tor redis: ", err)
ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end
return red
end
-- ------------------
-- generate redis key
-- ------------------
function generate_key(username)
return ngx.var.server_name .. ':' .. ngx.var.server_port .. ':' .. ngx.var.document_root .. ':' .. username
end
-- --------------------------------
-- password hashing/verify function
-- --------------------------------
function password_hash(password, salt)
-- yes this can be improved by using sha512, bcrypt, etc
local salt2 = ngx.sha1_bin(salt .. ':' .. password)
local digest = ngx.hmac_sha1(salt2, password)
return ngx.encode_base64(digest)
end
function password_verify(password, hash, salt)
return hash == password_hash(password, salt)
end
-- -----------------------------------------
-- validate user credentials with otp server
-- -----------------------------------------
function privacyidea_validate(username, password)
local httpc = require("resty.http").new()
-- prepare parameter
local params = {user = username, pass = password}
local realm = ngx.var.privacyidea_realm or nil
if realm then
params['realm'] = realm
end
-- send request
local uri = ngx.var.privacyidea_uri or '/privacyidea-validate-check'
local res, err = httpc:request_uri(uri, {
method = "POST",
body = ngx.encode_args(params),
headers = {
["Content-Type"] = "application/x-www-form-urlencoded",
},
})
if not res then
return false, 'privacyIDEA HTTP Request Error ' .. err
end
if res.status ~= 200 then
return false, 'privacyIDEA HTTP Status ' .. res.status
end
local cjson = require "cjson"
local answer = cjson.decode(res.body)
local ok = false
local err = ''
--if res.status ~= 200 and type(answer.result) == "table" then
if type(answer.result) == "table" then
-- authentication okay
if answer.result.status == true then
if answer.result.value == true then
ok = true
return ok, err
end
end
-- check for errors
if type(answer.detail) == "table" then
if answer.detail.message then
err = err .. ': ' .. answer.detail.message
end
end
if answer.result.error then
if answer.result.error.message then
err = err .. ': ' .. answer.result.error.message
end
end
end
return ok, err
end
-- --------------------
-- authentication logic
-- --------------------
function authenticate()
-- Test Authentication header is set and with a value
local header = ngx.req.get_headers()['Authorization']
if header == nil or header:find(" ") == nil then
return false
end
-- extract auth credentials
local username, password = extract_credentials()
if username == false then
return false
end
-- open redis connection
local redis_host = ngx.var.privacyidea_redis_host or '127.0.0.1'
local redis_port = ngx.var.privacyidea_redis_port or 6379
local ttl = ngx.var.privacyidea_ttl or 900
local red = redis_connect(redis_host, redis_port)
-- lookup key and hash
local key = generate_key(username)
local value = red:get(key)
-- password hash ok => extend ttl and return username if status is ok
if value and password_verify(password, value, key) then
red:expire(key,ttl)
return username
-- start remote authentication
else
local ok, err = privacyidea_validate(username, password)
if ok then
ngx.log(ngx.ERR, '[' .. username .. '] authentication ok')
red:setex(key, ttl, password_hash(password, key))
return username
else
ngx.log(ngx.ERR, '[' .. username .. '] invalid authentication' .. err)
return false
end
end
end
-- -------------------------
-- start authentication flow
-- -------------------------
local user = authenticate()
if not user then
local http_realm = ngx.var.privacyidea_http_realm or ''
ngx.header.www_authenticate = 'Basic realm="' .. http_realm .. '"'
ngx.exit(ngx.HTTP_UNAUTHORIZED)
return
end