Software /
code /
prosody-modules
Comparison
mod_auth_internal_yubikey/mod_auth_internal_yubikey.lua @ 341:f801ce6826d5
mod_auth_internal_yubikey: New authentication provider for two-factor authentication with Yubikeys
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Wed, 16 Feb 2011 22:04:55 +0000 |
child | 814:881ec9919144 |
comparison
equal
deleted
inserted
replaced
340:5d306466f3f6 | 341:f801ce6826d5 |
---|---|
1 -- Prosody IM | |
2 -- Copyright (C) 2008-2010 Matthew Wild | |
3 -- Copyright (C) 2008-2010 Waqas Hussain | |
4 -- | |
5 -- This project is MIT/X11 licensed. Please see the | |
6 -- COPYING file in the source package for more information. | |
7 -- | |
8 | |
9 local datamanager = require "util.datamanager"; | |
10 local storagemanager = require "core.storagemanager"; | |
11 local log = require "util.logger".init("auth_internal_yubikey"); | |
12 local type = type; | |
13 local error = error; | |
14 local ipairs = ipairs; | |
15 local hashes = require "util.hashes"; | |
16 local jid = require "util.jid"; | |
17 local jid_bare = require "util.jid".bare; | |
18 local config = require "core.configmanager"; | |
19 local usermanager = require "core.usermanager"; | |
20 local new_sasl = require "util.sasl".new; | |
21 local nodeprep = require "util.encodings".stringprep.nodeprep; | |
22 local hosts = hosts; | |
23 | |
24 local prosody = _G.prosody; | |
25 | |
26 local yubikey = require "yubikey".new_authenticator({ | |
27 prefix_length = module:get_option_number("yubikey_prefix_length", 0); | |
28 check_credentials = function (ret, state, data) | |
29 local account = data.account; | |
30 local yubikey_hash = hashes.sha1(ret.public_id..ret.private_id..(ret.password or ""), true); | |
31 if yubikey_hash == account.yubikey_hash then | |
32 return true; | |
33 end | |
34 return false, "invalid-otp"; | |
35 end; | |
36 store_device_info = function (state, data) | |
37 local new_account = {}; | |
38 for k, v in pairs(data.account) do | |
39 new_account[k] = v; | |
40 end | |
41 new_account.yubikey_state = state; | |
42 datamanager.store(data.username, data.host, "accounts", new_account); | |
43 end; | |
44 }); | |
45 | |
46 local global_yubikey_key = module:get_option_string("yubikey_key"); | |
47 | |
48 function new_default_provider(host) | |
49 local provider = { name = "internal_yubikey" }; | |
50 log("debug", "initializing default authentication provider for host '%s'", host); | |
51 | |
52 function provider.test_password(username, password) | |
53 log("debug", "test password '%s' for user %s at host %s", password, username, module.host); | |
54 | |
55 local account_info = datamanager.load(username, host, "accounts") or {}; | |
56 local yubikey_key = account_info.yubikey_key or global_yubikey_key; | |
57 if account_info.yubikey_key then | |
58 log("debug", "Authenticating Yubikey OTP for %s", username); | |
59 local authed, err = yubikey:authenticate(password, account_info.yubikey_key, account_info.yubikey_state or {}, { account = account_info, username = username, host = host }); | |
60 if not authed then | |
61 log("debug", "Failed to authenticate %s via OTP: %s", username, err); | |
62 return authed, err; | |
63 end | |
64 return authed; | |
65 elseif account_info.password and password == account_info.password then | |
66 -- No yubikey configured for this user, treat as normal password | |
67 log("debug", "No yubikey configured for %s, successful login using password auth", username); | |
68 return true; | |
69 else | |
70 return nil, "Auth failed. Invalid username or password."; | |
71 end | |
72 end | |
73 | |
74 function provider.get_password(username) | |
75 log("debug", "get_password for username '%s' at host '%s'", username, module.host); | |
76 return (datamanager.load(username, host, "accounts") or {}).password; | |
77 end | |
78 | |
79 function provider.set_password(username, password) | |
80 local account = datamanager.load(username, host, "accounts"); | |
81 if account then | |
82 account.password = password; | |
83 return datamanager.store(username, host, "accounts", account); | |
84 end | |
85 return nil, "Account not available."; | |
86 end | |
87 | |
88 function provider.user_exists(username) | |
89 local account = datamanager.load(username, host, "accounts"); | |
90 if not account then | |
91 log("debug", "account not found for username '%s' at host '%s'", username, module.host); | |
92 return nil, "Auth failed. Invalid username"; | |
93 end | |
94 return true; | |
95 end | |
96 | |
97 function provider.create_user(username, password) | |
98 return datamanager.store(username, host, "accounts", {password = password}); | |
99 end | |
100 | |
101 function provider.delete_user(username) | |
102 return datamanager.store(username, host, "accounts", nil); | |
103 end | |
104 | |
105 function provider.get_sasl_handler() | |
106 local realm = module:get_option("sasl_realm") or module.host; | |
107 local getpass_authentication_profile = { | |
108 plain_test = function(sasl, username, password, realm) | |
109 local prepped_username = nodeprep(username); | |
110 if not prepped_username then | |
111 log("debug", "NODEprep failed on username: %s", username); | |
112 return false, nil; | |
113 end | |
114 | |
115 return usermanager.test_password(username, realm, password), true; | |
116 end | |
117 }; | |
118 return new_sasl(realm, getpass_authentication_profile); | |
119 end | |
120 | |
121 return provider; | |
122 end | |
123 | |
124 module:add_item("auth-provider", new_default_provider(module.host)); | |
125 | |
126 function module.command(arg) | |
127 local command = arg[1]; | |
128 table.remove(arg, 1); | |
129 if command == "associate" then | |
130 local user_jid = arg[1]; | |
131 if not user_jid or user_jid == "help" then | |
132 prosodyctl.show_usage([[mod_auth_internal_yubikey associate JID]], [[Set the Yubikey details for a user]]); | |
133 return 1; | |
134 end | |
135 | |
136 local username, host = jid.prepped_split(user_jid); | |
137 if not username or not host then | |
138 print("Invalid JID: "..user_jid); | |
139 return 1; | |
140 end | |
141 | |
142 local password, public_id, private_id, key; | |
143 | |
144 for i=2,#arg do | |
145 local k, v = arg[i]:match("^%-%-(%w+)=(.*)$"); | |
146 if not k then | |
147 k, v = arg[i]:match("^%-(%w)(.*)$"); | |
148 end | |
149 if k == "password" then | |
150 password = v; | |
151 elseif k == "fixed" then | |
152 public_id = v; | |
153 elseif k == "uid" then | |
154 private_id = v; | |
155 elseif k == "key" or k == "a" then | |
156 key = v; | |
157 end | |
158 end | |
159 | |
160 if not password then | |
161 print(":: Password ::"); | |
162 print("This is an optional password that should be always"); | |
163 print("entered during login *before* the yubikey password."); | |
164 print("If the yubikey is lost/stolen, unless the attacker"); | |
165 print("knows this prefix, they cannot access the account."); | |
166 print(""); | |
167 password = prosodyctl.read_password(); | |
168 if not password then | |
169 print("Cancelled."); | |
170 return 1; | |
171 end | |
172 end | |
173 | |
174 if not public_id then | |
175 print(":: Public Yubikey ID ::"); | |
176 print("This is a fixed string of characters between 0 and 16"); | |
177 print("bytes long that the Yubikey prefixes to every token."); | |
178 print("The ID should be entered in modhex encoding, meaning "); | |
179 print("a string up to 32 characters. This *must* match"); | |
180 print("exactly the fixed string programmed into the yubikey."); | |
181 print(""); | |
182 io.write("Enter fixed id (modhex): "); | |
183 while true do | |
184 public_id = io.read("*l"); | |
185 if #public_id > 32 then | |
186 print("The fixed id must be 32 characters or less. Please try again."); | |
187 elseif public_id:match("[^cbdefghijklnrtuv]") then | |
188 print("The fixed id contains invalid characters. It must be entered in modhex encoding. Please try again."); | |
189 else | |
190 break; | |
191 end | |
192 end | |
193 end | |
194 | |
195 if not private_id then | |
196 print(":: Private Yubikey ID ::"); | |
197 print("This is a fixed secret UID programmed into the yubikey"); | |
198 print("during configuration. It must be entered in hex (not modhex)"); | |
199 print("encoding. It is always 6 bytes long, which is 12 characters"); | |
200 print("in hex encoding."); | |
201 print(""); | |
202 while true do | |
203 io.write("Enter private UID (hex): "); | |
204 private_id = io.read("*l"); | |
205 if #private_id ~= 12 then | |
206 print("The id length must be 12 characters in hex encoding. Please try again."); | |
207 elseif private_id:match("%X") then | |
208 print("The key contains invalid characters - it must be in hex encoding (not modhex). Please try again."); | |
209 else | |
210 break; | |
211 end | |
212 end | |
213 end | |
214 | |
215 if not key then | |
216 print(":: AES Encryption Key ::"); | |
217 print("This is the secret key that the Yubikey uses to encrypt the"); | |
218 print("generated tokens. It is 32 characters in hex encoding."); | |
219 print(""); | |
220 while true do | |
221 io.write("Enter AES key (hex): "); | |
222 key = io.read("*l"); | |
223 if #key ~= 32 then | |
224 print("The key length must be 32 characters in hex encoding. Please try again."); | |
225 elseif key:match("%X") then | |
226 print("The key contains invalid characters - it must be in hex encoding (not modhex). Please try again."); | |
227 else | |
228 break; | |
229 end | |
230 end | |
231 end | |
232 | |
233 local hash = hashes.sha1(public_id..private_id..password, true); | |
234 local account = { | |
235 yubikey_hash = hash; | |
236 yubikey_key = key; | |
237 }; | |
238 storagemanager.initialize_host(host); | |
239 local ok, err = datamanager.store(username, host, "accounts", account); | |
240 if not ok then | |
241 print("Error saving configuration:"); | |
242 print("", err); | |
243 return 1; | |
244 end | |
245 print("Saved."); | |
246 return 0; | |
247 end | |
248 end |