mod_muc_webchat_url: Fix default url
[prosody-modules.git] / mod_auth_internal_yubikey / mod_auth_internal_yubikey.lua
blob05008f4c2700d6da6711c1a30e0ae487bfe976df
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 --
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 hosts = hosts;
23 local prosody = _G.prosody;
25 local yubikey = require "yubikey".new_authenticator({
26 prefix_length = module:get_option_number("yubikey_prefix_length", 0);
27 check_credentials = function (ret, state, data)
28 local account = data.account;
29 local yubikey_hash = hashes.sha1(ret.public_id..ret.private_id..(ret.password or ""), true);
30 if yubikey_hash == account.yubikey_hash then
31 return true;
32 end
33 return false, "invalid-otp";
34 end;
35 store_device_info = function (state, data)
36 local new_account = {};
37 for k, v in pairs(data.account) do
38 new_account[k] = v;
39 end
40 new_account.yubikey_state = state;
41 datamanager.store(data.username, data.host, "accounts", new_account);
42 end;
43 });
45 local global_yubikey_key = module:get_option_string("yubikey_key");
47 local host = module.host;
48 local provider = {};
49 log("debug", "initializing default authentication provider for host '%s'", host);
51 function provider.test_password(username, password)
52 log("debug", "test password '%s' for user %s at host %s", password, username, module.host);
54 local account_info = datamanager.load(username, host, "accounts") or {};
55 local yubikey_key = account_info.yubikey_key or global_yubikey_key;
56 if account_info.yubikey_key then
57 log("debug", "Authenticating Yubikey OTP for %s", username);
58 local authed, err = yubikey:authenticate(password, account_info.yubikey_key, account_info.yubikey_state or {}, { account = account_info, username = username, host = host });
59 if not authed then
60 log("debug", "Failed to authenticate %s via OTP: %s", username, err);
61 return authed, err;
62 end
63 return authed;
64 elseif account_info.password and password == account_info.password then
65 -- No yubikey configured for this user, treat as normal password
66 log("debug", "No yubikey configured for %s, successful login using password auth", username);
67 return true;
68 else
69 return nil, "Auth failed. Invalid username or password.";
70 end
71 end
73 function provider.get_password(username)
74 log("debug", "get_password for username '%s' at host '%s'", username, module.host);
75 return (datamanager.load(username, host, "accounts") or {}).password;
76 end
78 function provider.set_password(username, password)
79 local account = datamanager.load(username, host, "accounts");
80 if account then
81 account.password = password;
82 return datamanager.store(username, host, "accounts", account);
83 end
84 return nil, "Account not available.";
85 end
87 function provider.user_exists(username)
88 local account = datamanager.load(username, host, "accounts");
89 if not account then
90 log("debug", "account not found for username '%s' at host '%s'", username, module.host);
91 return nil, "Auth failed. Invalid username";
92 end
93 return true;
94 end
96 function provider.create_user(username, password)
97 return datamanager.store(username, host, "accounts", {password = password});
98 end
100 function provider.delete_user(username)
101 return datamanager.store(username, host, "accounts", nil);
104 function provider.get_sasl_handler()
105 local realm = module:get_option("sasl_realm") or module.host;
106 local getpass_authentication_profile = {
107 plain_test = function(sasl, username, password, realm)
108 return usermanager.test_password(username, realm, password), true;
111 return new_sasl(realm, getpass_authentication_profile);
114 module:provides("auth", provider);
116 function module.command(arg)
117 local command = arg[1];
118 table.remove(arg, 1);
119 if command == "associate" then
120 local user_jid = arg[1];
121 if not user_jid or user_jid == "help" then
122 prosodyctl.show_usage([[mod_auth_internal_yubikey associate JID]], [[Set the Yubikey details for a user]]);
123 return 1;
126 local username, host = jid.prepped_split(user_jid);
127 if not username or not host then
128 print("Invalid JID: "..user_jid);
129 return 1;
132 local password, public_id, private_id, key;
134 for i=2,#arg do
135 local k, v = arg[i]:match("^%-%-(%w+)=(.*)$");
136 if not k then
137 k, v = arg[i]:match("^%-(%w)(.*)$");
139 if k == "password" then
140 password = v;
141 elseif k == "fixed" then
142 public_id = v;
143 elseif k == "uid" then
144 private_id = v;
145 elseif k == "key" or k == "a" then
146 key = v;
150 if not password then
151 print(":: Password ::");
152 print("This is an optional password that should be always");
153 print("entered during login *before* the yubikey password.");
154 print("If the yubikey is lost/stolen, unless the attacker");
155 print("knows this prefix, they cannot access the account.");
156 print("");
157 password = prosodyctl.read_password();
158 if not password then
159 print("Cancelled.");
160 return 1;
164 if not public_id then
165 print(":: Public Yubikey ID ::");
166 print("This is a fixed string of characters between 0 and 16");
167 print("bytes long that the Yubikey prefixes to every token.");
168 print("The ID should be entered in modhex encoding, meaning ");
169 print("a string up to 32 characters. This *must* match");
170 print("exactly the fixed string programmed into the yubikey.");
171 print("");
172 io.write("Enter fixed id (modhex): ");
173 while true do
174 public_id = io.read("*l");
175 if #public_id > 32 then
176 print("The fixed id must be 32 characters or less. Please try again.");
177 elseif public_id:match("[^cbdefghijklnrtuv]") then
178 print("The fixed id contains invalid characters. It must be entered in modhex encoding. Please try again.");
179 else
180 break;
185 if not private_id then
186 print(":: Private Yubikey ID ::");
187 print("This is a fixed secret UID programmed into the yubikey");
188 print("during configuration. It must be entered in hex (not modhex)");
189 print("encoding. It is always 6 bytes long, which is 12 characters");
190 print("in hex encoding.");
191 print("");
192 while true do
193 io.write("Enter private UID (hex): ");
194 private_id = io.read("*l");
195 if #private_id ~= 12 then
196 print("The id length must be 12 characters in hex encoding. Please try again.");
197 elseif private_id:match("%X") then
198 print("The key contains invalid characters - it must be in hex encoding (not modhex). Please try again.");
199 else
200 break;
205 if not key then
206 print(":: AES Encryption Key ::");
207 print("This is the secret key that the Yubikey uses to encrypt the");
208 print("generated tokens. It is 32 characters in hex encoding.");
209 print("");
210 while true do
211 io.write("Enter AES key (hex): ");
212 key = io.read("*l");
213 if #key ~= 32 then
214 print("The key length must be 32 characters in hex encoding. Please try again.");
215 elseif key:match("%X") then
216 print("The key contains invalid characters - it must be in hex encoding (not modhex). Please try again.");
217 else
218 break;
223 local hash = hashes.sha1(public_id..private_id..password, true);
224 local account = {
225 yubikey_hash = hash;
226 yubikey_key = key;
228 storagemanager.initialize_host(host);
229 local ok, err = datamanager.store(username, host, "accounts", account);
230 if not ok then
231 print("Error saving configuration:");
232 print("", err);
233 return 1;
235 print("Saved.");
236 return 0;