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
parents
children 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