comparison mod_http_oauth2/mod_http_oauth2.lua @ 5650:0eb2d5ea2428

merge
author Stephen Paul Weber <singpolyma@singpolyma.net>
date Sat, 06 May 2023 19:40:23 -0500
parents aa068449b0b6
children 5b2352dda31f
comparison
equal deleted inserted replaced
5649:2c69577b28c2 5650:0eb2d5ea2428
4 local jid = require "util.jid"; 4 local jid = require "util.jid";
5 local json = require "util.json"; 5 local json = require "util.json";
6 local usermanager = require "core.usermanager"; 6 local usermanager = require "core.usermanager";
7 local errors = require "util.error"; 7 local errors = require "util.error";
8 local url = require "socket.url"; 8 local url = require "socket.url";
9 local uuid = require "util.uuid"; 9 local id = require "util.id";
10 local encodings = require "util.encodings"; 10 local encodings = require "util.encodings";
11 local base64 = encodings.base64; 11 local base64 = encodings.base64;
12 local random = require "util.random";
13 local schema = require "util.jsonschema";
14 local set = require "util.set";
15 local jwt = require"util.jwt";
16 local it = require "util.iterators";
17 local array = require "util.array";
18 local st = require "util.stanza";
19
20 local function b64url(s)
21 return (base64.encode(s):gsub("[+/=]", { ["+"] = "-", ["/"] = "_", ["="] = "" }))
22 end
23
24 local function tmap(t)
25 return function(k)
26 return t[k];
27 end
28 end
29
30 local function read_file(base_path, fn, required)
31 local f, err = io.open(base_path .. "/" .. fn);
32 if not f then
33 module:log(required and "error" or "debug", "Unable to load template file: %s", err);
34 if required then
35 return error("Failed to load templates");
36 end
37 return nil;
38 end
39 local data = assert(f:read("*a"));
40 assert(f:close());
41 return data;
42 end
43
44 local template_path = module:get_option_path("oauth2_template_path", "html");
45 local templates = {
46 login = read_file(template_path, "login.html", true);
47 consent = read_file(template_path, "consent.html", true);
48 error = read_file(template_path, "error.html", true);
49 css = read_file(template_path, "style.css");
50 js = read_file(template_path, "script.js");
51 };
52
53 local site_name = module:get_option_string("site_name", module.host);
54
55 local _render_html = require"util.interpolation".new("%b{}", st.xml_escape);
56 local function render_page(template, data, sensitive)
57 data = data or {};
58 data.site_name = site_name;
59 local resp = {
60 status_code = 200;
61 headers = {
62 ["Content-Type"] = "text/html; charset=utf-8";
63 ["Content-Security-Policy"] = "default-src 'self'";
64 ["X-Frame-Options"] = "DENY";
65 ["Cache-Control"] = (sensitive and "no-store" or "no-cache")..", private";
66 };
67 body = _render_html(template, data);
68 };
69 return resp;
70 end
12 71
13 local tokens = module:depends("tokenauth"); 72 local tokens = module:depends("tokenauth");
14 73
15 local clients = module:open_store("oauth2_clients", "map"); 74 local default_access_ttl = module:get_option_number("oauth2_access_token_ttl", 86400);
16 75 local default_refresh_ttl = module:get_option_number("oauth2_refresh_token_ttl", nil);
17 local function filter_scopes(username, host, requested_scope_string) 76
18 if host ~= module.host then 77 -- Used to derive client_secret from client_id, set to enable stateless dynamic registration.
19 return usermanager.get_jid_role(username.."@"..host, module.host).name; 78 local registration_key = module:get_option_string("oauth2_registration_key");
20 end 79 local registration_algo = module:get_option_string("oauth2_registration_algorithm", "HS256");
21 80 local registration_ttl = module:get_option("oauth2_registration_ttl", nil);
22 if requested_scope_string then -- Specific role requested 81 local registration_options = module:get_option("oauth2_registration_options",
23 -- TODO: The requested scope string is technically a space-delimited list 82 { default_ttl = registration_ttl; accept_expired = not registration_ttl });
24 -- of scopes, but for simplicity we're mapping this slot to role names. 83
25 if usermanager.user_can_assume_role(username, module.host, requested_scope_string) then 84 local pkce_required = module:get_option_boolean("oauth2_require_code_challenge", false);
26 return requested_scope_string; 85
27 end 86 local verification_key;
28 end 87 local jwt_sign, jwt_verify;
29 88 if registration_key then
89 -- Tie it to the host if global
90 verification_key = hashes.hmac_sha256(registration_key, module.host);
91 jwt_sign, jwt_verify = jwt.init(registration_algo, registration_key, registration_key, registration_options);
92 end
93
94 local function parse_scopes(scope_string)
95 return array(scope_string:gmatch("%S+"));
96 end
97
98 local openid_claims = set.new({ "openid", "profile"; "email"; "address"; "phone" });
99
100 local function split_scopes(scope_list)
101 local claims, roles, unknown = array(), array(), array();
102 local all_roles = usermanager.get_all_roles(module.host);
103 for _, scope in ipairs(scope_list) do
104 if openid_claims:contains(scope) then
105 claims:push(scope);
106 elseif all_roles[scope] then
107 roles:push(scope);
108 else
109 unknown:push(scope);
110 end
111 end
112 return claims, roles, unknown;
113 end
114
115 local function can_assume_role(username, requested_role)
116 return usermanager.user_can_assume_role(username, module.host, requested_role);
117 end
118
119 local function select_role(username, requested_roles)
120 if requested_roles then
121 for _, requested_role in ipairs(requested_roles) do
122 if can_assume_role(username, requested_role) then
123 return requested_role;
124 end
125 end
126 end
127 -- otherwise the default role
30 return usermanager.get_user_role(username, module.host).name; 128 return usermanager.get_user_role(username, module.host).name;
31 end 129 end
32 130
33 local function code_expires_in(code) 131 local function filter_scopes(username, requested_scope_string)
34 return os.difftime(os.time(), code.issued); 132 local granted_scopes, requested_roles;
35 end 133
36 134 if requested_scope_string then -- Specific role(s) requested
37 local function code_expired(code) 135 granted_scopes, requested_roles = split_scopes(parse_scopes(requested_scope_string));
38 return code_expires_in(code) > 120; 136 else
137 granted_scopes = array();
138 end
139
140 local selected_role = select_role(username, requested_roles);
141 granted_scopes:push(selected_role);
142
143 return granted_scopes:concat(" "), selected_role;
144 end
145
146 local function code_expires_in(code) --> number, seconds until code expires
147 return os.difftime(code.expires, os.time());
148 end
149
150 local function code_expired(code) --> boolean, true: has expired, false: still valid
151 return code_expires_in(code) < 0;
39 end 152 end
40 153
41 local codes = cache.new(10000, function (_, code) 154 local codes = cache.new(10000, function (_, code)
42 return code_expired(code) 155 return code_expired(code)
43 end); 156 end);
44 157
45 module:add_timer(900, function() 158 -- Periodically clear out unredeemed codes. Does not need to be exact, expired
159 -- codes are rejected if tried. Mostly just to keep memory usage in check.
160 module:hourly("Clear expired authorization codes", function()
46 local k, code = codes:tail(); 161 local k, code = codes:tail();
47 while code and code_expired(code) do 162 while code and code_expired(code) do
48 codes:set(k, nil); 163 codes:set(k, nil);
49 k, code = codes:tail(); 164 k, code = codes:tail();
50 end 165 end
51 return code and code_expires_in(code) + 1 or 900;
52 end) 166 end)
167
168 local function get_issuer()
169 return (module:http_url(nil, "/"):gsub("/$", ""));
170 end
171
172 local loopbacks = set.new({ "localhost", "127.0.0.1", "::1" });
173 local function is_secure_redirect(uri)
174 local u = url.parse(uri);
175 return u.scheme ~= "http" or loopbacks:contains(u.host);
176 end
53 177
54 local function oauth_error(err_name, err_desc) 178 local function oauth_error(err_name, err_desc)
55 return errors.new({ 179 return errors.new({
56 type = "modify"; 180 type = "modify";
57 condition = "bad-request"; 181 condition = "bad-request";
59 text = err_desc and (err_name..": "..err_desc) or err_name; 183 text = err_desc and (err_name..": "..err_desc) or err_name;
60 extra = { oauth2_response = { error = err_name, error_description = err_desc } }; 184 extra = { oauth2_response = { error = err_name, error_description = err_desc } };
61 }); 185 });
62 end 186 end
63 187
64 local function new_access_token(token_jid, scope, ttl) 188 -- client_id / client_metadata are pretty large, filter out a subset of
65 local token = tokens.create_jid_token(token_jid, token_jid, scope, ttl); 189 -- properties that are deemed useful e.g. in case tokens issued to a certain
190 -- client needs to be revoked
191 local function client_subset(client)
192 return { name = client.client_name; uri = client.client_uri; id = client.software_id; version = client.software_version };
193 end
194
195 local function new_access_token(token_jid, role, scope_string, client, id_token, refresh_token_info)
196 local token_data = { oauth2_scopes = scope_string, oauth2_client = nil };
197 if client then
198 token_data.oauth2_client = client_subset(client);
199 end
200 if next(token_data) == nil then
201 token_data = nil;
202 end
203
204 local refresh_token;
205 local grant = refresh_token_info and refresh_token_info.grant;
206 if not grant then
207 -- No existing grant, create one
208 grant = tokens.create_grant(token_jid, token_jid, default_refresh_ttl, token_data);
209 -- Create refresh token for the grant if desired
210 refresh_token = refresh_token_info ~= false and tokens.create_token(token_jid, grant, nil, nil, "oauth2-refresh");
211 else
212 -- Grant exists, reuse existing refresh token
213 refresh_token = refresh_token_info.token;
214
215 refresh_token_info.grant = nil; -- Prevent reference loop
216 end
217
218 local access_token, access_token_info = tokens.create_token(token_jid, grant, role, default_access_ttl, "oauth2");
219
220 local expires_at = access_token_info.expires;
66 return { 221 return {
67 token_type = "bearer"; 222 token_type = "bearer";
68 access_token = token; 223 access_token = access_token;
69 expires_in = ttl; 224 expires_in = expires_at and (expires_at - os.time()) or nil;
70 scope = scope; 225 scope = scope_string;
71 -- TODO: include refresh_token when implemented 226 id_token = id_token;
227 refresh_token = refresh_token or nil;
72 }; 228 };
229 end
230
231 local function get_redirect_uri(client, query_redirect_uri) -- record client, string : string
232 if not query_redirect_uri then
233 if #client.redirect_uris ~= 1 then
234 -- Client registered multiple URIs, it needs specify which one to use
235 return;
236 end
237 -- When only a single URI is registered, that's the default
238 return client.redirect_uris[1];
239 end
240 -- Verify the client-provided URI matches one previously registered
241 for _, redirect_uri in ipairs(client.redirect_uris) do
242 if query_redirect_uri == redirect_uri then
243 return redirect_uri
244 end
245 end
73 end 246 end
74 247
75 local grant_type_handlers = {}; 248 local grant_type_handlers = {};
76 local response_type_handlers = {}; 249 local response_type_handlers = {};
250 local verifier_transforms = {};
77 251
78 function grant_type_handlers.password(params) 252 function grant_type_handlers.password(params)
79 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); 253 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)"));
80 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); 254 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'"));
81 local request_username, request_host, request_resource = jid.prepped_split(request_jid); 255 local request_username, request_host, request_resource = jid.prepped_split(request_jid);
86 if not usermanager.test_password(request_username, request_host, request_password) then 260 if not usermanager.test_password(request_username, request_host, request_password) then
87 return oauth_error("invalid_grant", "incorrect credentials"); 261 return oauth_error("invalid_grant", "incorrect credentials");
88 end 262 end
89 263
90 local granted_jid = jid.join(request_username, request_host, request_resource); 264 local granted_jid = jid.join(request_username, request_host, request_resource);
91 local granted_scopes = filter_scopes(request_username, request_host, params.scope); 265 local granted_scopes, granted_role = filter_scopes(request_username, params.scope);
92 return json.encode(new_access_token(granted_jid, granted_scopes, nil)); 266 return json.encode(new_access_token(granted_jid, granted_role, granted_scopes, nil));
93 end 267 end
94 268
95 function response_type_handlers.code(params, granted_jid) 269 function response_type_handlers.code(client, params, granted_jid, id_token)
96 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end 270 local request_username, request_host = jid.split(granted_jid);
97 if not params.redirect_uri then return oauth_error("invalid_request", "missing 'redirect_uri'"); end 271 if not request_host or request_host ~= module.host then
98 272 return oauth_error("invalid_request", "invalid JID");
99 local client_owner, client_host, client_id = jid.prepped_split(params.client_id); 273 end
100 if client_host ~= module.host then 274 local granted_scopes, granted_role = filter_scopes(request_username, params.scope);
101 return oauth_error("invalid_client", "incorrect credentials"); 275
102 end 276 if pkce_required and not params.code_challenge then
103 local client, err = clients:get(client_owner, client_id); 277 return oauth_error("invalid_request", "PKCE required");
104 if err then error(err); end 278 end
105 if not client then 279
106 return oauth_error("invalid_client", "incorrect credentials"); 280 local code = id.medium();
107 end
108
109 local granted_scopes = filter_scopes(client_owner, client_host, params.scope);
110
111 local code = uuid.generate();
112 local ok = codes:set(params.client_id .. "#" .. code, { 281 local ok = codes:set(params.client_id .. "#" .. code, {
113 issued = os.time(); 282 expires = os.time() + 600;
114 granted_jid = granted_jid; 283 granted_jid = granted_jid;
115 granted_scopes = granted_scopes; 284 granted_scopes = granted_scopes;
285 granted_role = granted_role;
286 challenge = params.code_challenge;
287 challenge_method = params.code_challenge_method;
288 id_token = id_token;
116 }); 289 });
117 if not ok then 290 if not ok then
118 return {status_code = 429}; 291 return {status_code = 429};
119 end 292 end
120 293
121 local redirect = url.parse(params.redirect_uri); 294 local redirect_uri = get_redirect_uri(client, params.redirect_uri);
295 if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" then
296 -- TODO some nicer template page
297 -- mod_http_errors will set content-type to text/html if it catches this
298 -- event, if not text/plain is kept for the fallback text.
299 local response = { status_code = 200; headers = { content_type = "text/plain" } }
300 response.body = module:context("*"):fire_event("http-message", {
301 response = response;
302 title = "Your authorization code";
303 message = "Here's your authorization code, copy and paste it into " .. (client.client_name or "your client");
304 extra = code;
305 }) or ("Here's your authorization code:\n%s\n"):format(code);
306 return response;
307 elseif not redirect_uri then
308 return 400;
309 end
310
311 local redirect = url.parse(redirect_uri);
312
122 local query = http.formdecode(redirect.query or ""); 313 local query = http.formdecode(redirect.query or "");
123 if type(query) ~= "table" then query = {}; end 314 if type(query) ~= "table" then query = {}; end
124 table.insert(query, { name = "code", value = code }) 315 table.insert(query, { name = "code", value = code });
316 table.insert(query, { name = "iss", value = get_issuer() });
125 if params.state then 317 if params.state then
126 table.insert(query, { name = "state", value = params.state }); 318 table.insert(query, { name = "state", value = params.state });
127 end 319 end
128 redirect.query = http.formencode(query); 320 redirect.query = http.formencode(query);
129 321
130 return { 322 return {
131 status_code = 302; 323 status_code = 303;
132 headers = { 324 headers = {
133 location = url.build(redirect); 325 location = url.build(redirect);
134 }; 326 };
135 } 327 }
136 end 328 end
137 329
138 local pepper = module:get_option_string("oauth2_client_pepper", ""); 330 -- Implicit flow
139 331 function response_type_handlers.token(client, params, granted_jid)
140 local function verify_secret(stored, salt, i, secret) 332 local request_username, request_host = jid.split(granted_jid);
141 return base64.decode(stored) == hashes.pbkdf2_hmac_sha256(secret, salt .. pepper, i); 333 if not request_host or request_host ~= module.host then
334 return oauth_error("invalid_request", "invalid JID");
335 end
336 local granted_scopes, granted_role = filter_scopes(request_username, params.scope);
337 local token_info = new_access_token(granted_jid, granted_role, granted_scopes, client, nil);
338
339 local redirect = url.parse(get_redirect_uri(client, params.redirect_uri));
340 if not redirect then return 400; end
341 token_info.state = params.state;
342 redirect.fragment = http.formencode(token_info);
343
344 return {
345 status_code = 303;
346 headers = {
347 location = url.build(redirect);
348 };
349 }
350 end
351
352 local function make_client_secret(client_id) --> client_secret
353 return hashes.hmac_sha256(verification_key, client_id, true);
354 end
355
356 local function verify_client_secret(client_id, client_secret)
357 return hashes.equals(make_client_secret(client_id), client_secret);
142 end 358 end
143 359
144 function grant_type_handlers.authorization_code(params) 360 function grant_type_handlers.authorization_code(params)
145 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end 361 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end
146 if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end 362 if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end
147 if not params.code then return oauth_error("invalid_request", "missing 'code'"); end 363 if not params.code then return oauth_error("invalid_request", "missing 'code'"); end
148 if params.scope and params.scope ~= "" then 364 if params.scope and params.scope ~= "" then
149 return oauth_error("invalid_scope", "unknown scope requested"); 365 return oauth_error("invalid_scope", "unknown scope requested");
150 end 366 end
151 367
152 local client_owner, client_host, client_id = jid.prepped_split(params.client_id); 368 local client_ok, client = jwt_verify(params.client_id);
153 if client_host ~= module.host then 369 if not client_ok then
154 module:log("debug", "%q ~= %q", client_host, module.host);
155 return oauth_error("invalid_client", "incorrect credentials"); 370 return oauth_error("invalid_client", "incorrect credentials");
156 end 371 end
157 local client, err = clients:get(client_owner, client_id); 372
158 if err then error(err); end 373 if not verify_client_secret(params.client_id, params.client_secret) then
159 if not client or not verify_secret(client.secret_hash, client.salt, client.iteration_count, params.client_secret) then
160 module:log("debug", "client_secret mismatch"); 374 module:log("debug", "client_secret mismatch");
161 return oauth_error("invalid_client", "incorrect credentials"); 375 return oauth_error("invalid_client", "incorrect credentials");
162 end 376 end
163 local code, err = codes:get(params.client_id .. "#" .. params.code); 377 local code, err = codes:get(params.client_id .. "#" .. params.code);
164 if err then error(err); end 378 if err then error(err); end
379 -- MUST NOT use the authorization code more than once, so remove it to
380 -- prevent a second attempted use
381 codes:set(params.client_id .. "#" .. params.code, nil);
165 if not code or type(code) ~= "table" or code_expired(code) then 382 if not code or type(code) ~= "table" or code_expired(code) then
166 module:log("debug", "authorization_code invalid or expired: %q", code); 383 module:log("debug", "authorization_code invalid or expired: %q", code);
167 return oauth_error("invalid_client", "incorrect credentials"); 384 return oauth_error("invalid_client", "incorrect credentials");
168 end 385 end
169 assert(codes:set(client_owner, client_id .. "#" .. params.code, nil)); 386
170 387 -- TODO Decide if the code should be removed or not when PKCE fails
171 return json.encode(new_access_token(code.granted_jid, code.granted_scopes, nil)); 388 local transform = verifier_transforms[code.challenge_method or "plain"];
172 end 389 if not transform then
173 390 return oauth_error("invalid_request", "unknown challenge transform method");
174 local function check_credentials(request, allow_token) 391 elseif transform(params.code_verifier) ~= code.challenge then
392 return oauth_error("invalid_grant", "incorrect credentials");
393 end
394
395 return json.encode(new_access_token(code.granted_jid, code.granted_role, code.granted_scopes, client, code.id_token));
396 end
397
398 function grant_type_handlers.refresh_token(params)
399 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end
400 if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end
401 if not params.refresh_token then return oauth_error("invalid_request", "missing 'refresh_token'"); end
402
403 local client_ok, client = jwt_verify(params.client_id);
404 if not client_ok then
405 return oauth_error("invalid_client", "incorrect credentials");
406 end
407
408 if not verify_client_secret(params.client_id, params.client_secret) then
409 module:log("debug", "client_secret mismatch");
410 return oauth_error("invalid_client", "incorrect credentials");
411 end
412
413 local refresh_token_info = tokens.get_token_info(params.refresh_token);
414 if not refresh_token_info or refresh_token_info.purpose ~= "oauth2-refresh" then
415 return oauth_error("invalid_grant", "invalid refresh token");
416 end
417
418 -- new_access_token() requires the actual token
419 refresh_token_info.token = params.refresh_token;
420
421 return json.encode(new_access_token(
422 refresh_token_info.jid, refresh_token_info.role, refresh_token_info.grant.data.oauth2_scopes, client, nil, refresh_token_info
423 ));
424 end
425
426 -- RFC 7636 Proof Key for Code Exchange by OAuth Public Clients
427
428 function verifier_transforms.plain(code_verifier)
429 -- code_challenge = code_verifier
430 return code_verifier;
431 end
432
433 function verifier_transforms.S256(code_verifier)
434 -- code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
435 return code_verifier and b64url(hashes.sha256(code_verifier));
436 end
437
438 -- Used to issue/verify short-lived tokens for the authorization process below
439 local new_user_token, verify_user_token = jwt.init("HS256", random.bytes(32), nil, { default_ttl = 600 });
440
441 -- From the given request, figure out if the user is authenticated and has granted consent yet
442 -- As this requires multiple steps (seek credentials, seek consent), we have a lot of state to
443 -- carry around across requests. We also need to protect against CSRF and session mix-up attacks
444 -- (e.g. the user may have multiple concurrent flows in progress, session cookies aren't unique
445 -- to one of them).
446 -- Our strategy here is to preserve the original query string (containing the authz request), and
447 -- encode the rest of the flow in form POSTs.
448 local function get_auth_state(request)
449 local form = request.method == "POST"
450 and request.body
451 and request.body ~= ""
452 and request.headers.content_type == "application/x-www-form-urlencoded"
453 and http.formdecode(request.body);
454
455 if type(form) ~= "table" then return {}; end
456
457 if not form.user_token then
458 -- First step: login
459 local username = encodings.stringprep.nodeprep(form.username);
460 local password = encodings.stringprep.saslprep(form.password);
461 if not (username and password) or not usermanager.test_password(username, module.host, password) then
462 return {
463 error = "Invalid username/password";
464 };
465 end
466 return {
467 user = {
468 username = username;
469 host = module.host;
470 token = new_user_token({ username = username, host = module.host });
471 };
472 };
473 elseif form.user_token and form.consent then
474 -- Second step: consent
475 local ok, user = verify_user_token(form.user_token);
476 if not ok then
477 return {
478 error = user == "token-expired" and "Session expired - try again" or nil;
479 };
480 end
481
482 local scope = array():append(form):filter(function(field)
483 return field.name == "scope" or field.name == "role";
484 end):pluck("value"):concat(" ");
485
486 user.token = form.user_token;
487 return {
488 user = user;
489 scope = scope;
490 consent = form.consent == "granted";
491 };
492 end
493
494 return {};
495 end
496
497 local function get_request_credentials(request)
498 if not request.headers.authorization then return; end
499
175 local auth_type, auth_data = string.match(request.headers.authorization, "^(%S+)%s(.+)$"); 500 local auth_type, auth_data = string.match(request.headers.authorization, "^(%S+)%s(.+)$");
176 501
177 if auth_type == "Basic" then 502 if auth_type == "Basic" then
178 local creds = base64.decode(auth_data); 503 local creds = base64.decode(auth_data);
179 if not creds then return false; end 504 if not creds then return; end
180 local username, password = string.match(creds, "^([^:]+):(.*)$"); 505 local username, password = string.match(creds, "^([^:]+):(.*)$");
181 if not username then return false; end 506 if not username then return; end
182 username, password = encodings.stringprep.nodeprep(username), encodings.stringprep.saslprep(password); 507 return {
183 if not username then return false; end 508 type = "basic";
184 if not usermanager.test_password(username, module.host, password) then 509 username = username;
185 return false; 510 password = password;
186 end 511 };
187 return username; 512 elseif auth_type == "Bearer" then
188 elseif auth_type == "Bearer" and allow_token then 513 return {
189 local token_info = tokens.get_token_info(auth_data); 514 type = "bearer";
190 if not token_info or not token_info.session or token_info.session.host ~= module.host then 515 bearer_token = auth_data;
191 return false; 516 };
192 end 517 end
193 return token_info.session.username; 518
194 end
195 return nil; 519 return nil;
196 end 520 end
197 521
198 if module:get_host_type() == "component" then 522 if module:get_host_type() == "component" then
199 local component_secret = assert(module:get_option_string("component_secret"), "'component_secret' is a required setting when loaded on a Component"); 523 local component_secret = assert(module:get_option_string("component_secret"), "'component_secret' is a required setting when loaded on a Component");
208 if not request_host or request_host ~= module.host then 532 if not request_host or request_host ~= module.host then
209 return oauth_error("invalid_request", "invalid JID"); 533 return oauth_error("invalid_request", "invalid JID");
210 end 534 end
211 if request_password == component_secret then 535 if request_password == component_secret then
212 local granted_jid = jid.join(request_username, request_host, request_resource); 536 local granted_jid = jid.join(request_username, request_host, request_resource);
213 return json.encode(new_access_token(granted_jid, nil, nil)); 537 return json.encode(new_access_token(granted_jid, nil, nil, nil));
214 end 538 end
215 return oauth_error("invalid_grant", "incorrect credentials"); 539 return oauth_error("invalid_grant", "incorrect credentials");
216 end 540 end
217 541
218 -- TODO How would this make sense with components? 542 -- TODO How would this make sense with components?
219 -- Have an admin authenticate maybe? 543 -- Have an admin authenticate maybe?
220 response_type_handlers.code = nil; 544 response_type_handlers.code = nil;
545 response_type_handlers.token = nil;
221 grant_type_handlers.authorization_code = nil; 546 grant_type_handlers.authorization_code = nil;
222 check_credentials = function () return false end 547 end
548
549 -- OAuth errors should be returned to the client if possible, i.e. by
550 -- appending the error information to the redirect_uri and sending the
551 -- redirect to the user-agent. In some cases we can't do this, e.g. if
552 -- the redirect_uri is missing or invalid. In those cases, we render an
553 -- error directly to the user-agent.
554 local function error_response(request, err)
555 local q = request.url.query and http.formdecode(request.url.query);
556 local redirect_uri = q and q.redirect_uri;
557 if not redirect_uri or not is_secure_redirect(redirect_uri) then
558 module:log("warn", "Missing or invalid redirect_uri <%s>, rendering error to user-agent", redirect_uri or "");
559 return render_page(templates.error, { error = err });
560 end
561 local redirect_query = url.parse(redirect_uri);
562 local sep = redirect_query.query and "&" or "?";
563 redirect_uri = redirect_uri
564 .. sep .. http.formencode(err.extra.oauth2_response)
565 .. "&" .. http.formencode({ state = q.state, iss = get_issuer() });
566 module:log("warn", "Sending error response to client via redirect to %s", redirect_uri);
567 return {
568 status_code = 303;
569 headers = {
570 location = redirect_uri;
571 };
572 };
573 end
574
575 local allowed_grant_type_handlers = module:get_option_set("allowed_oauth2_grant_types", {"authorization_code", "password", "refresh_token"})
576 for handler_type in pairs(grant_type_handlers) do
577 if not allowed_grant_type_handlers:contains(handler_type) then
578 module:log("debug", "Grant type %q disabled", handler_type);
579 grant_type_handlers[handler_type] = nil;
580 else
581 module:log("debug", "Grant type %q enabled", handler_type);
582 end
583 end
584
585 -- "token" aka implicit flow is considered insecure
586 local allowed_response_type_handlers = module:get_option_set("allowed_oauth2_response_types", {"code"})
587 for handler_type in pairs(response_type_handlers) do
588 if not allowed_response_type_handlers:contains(handler_type) then
589 module:log("debug", "Response type %q disabled", handler_type);
590 response_type_handlers[handler_type] = nil;
591 else
592 module:log("debug", "Response type %q enabled", handler_type);
593 end
594 end
595
596 local allowed_challenge_methods = module:get_option_set("allowed_oauth2_code_challenge_methods", { "plain"; "S256" })
597 for handler_type in pairs(verifier_transforms) do
598 if not allowed_challenge_methods:contains(handler_type) then
599 module:log("debug", "Challenge method %q disabled", handler_type);
600 verifier_transforms[handler_type] = nil;
601 else
602 module:log("debug", "Challenge method %q enabled", handler_type);
603 end
223 end 604 end
224 605
225 function handle_token_grant(event) 606 function handle_token_grant(event)
607 local credentials = get_request_credentials(event.request);
608
226 event.response.headers.content_type = "application/json"; 609 event.response.headers.content_type = "application/json";
227 local params = http.formdecode(event.request.body); 610 local params = http.formdecode(event.request.body);
228 if not params then 611 if not params then
229 return oauth_error("invalid_request"); 612 return error_response(event.request, oauth_error("invalid_request"));
230 end 613 end
614
615 if credentials and credentials.type == "basic" then
616 -- client_secret_basic converted internally to client_secret_post
617 params.client_id = http.urldecode(credentials.username);
618 params.client_secret = http.urldecode(credentials.password);
619 end
620
231 local grant_type = params.grant_type 621 local grant_type = params.grant_type
232 local grant_handler = grant_type_handlers[grant_type]; 622 local grant_handler = grant_type_handlers[grant_type];
233 if not grant_handler then 623 if not grant_handler then
234 return oauth_error("unsupported_grant_type"); 624 return error_response(event.request, oauth_error("unsupported_grant_type"));
235 end 625 end
236 return grant_handler(params); 626 return grant_handler(params);
237 end 627 end
238 628
239 local function handle_authorization_request(event) 629 local function handle_authorization_request(event)
240 local request, response = event.request, event.response; 630 local request = event.request;
241 if not request.headers.authorization then 631
242 response.headers.www_authenticate = string.format("Basic realm=%q", module.host.."/"..module.name);
243 return 401;
244 end
245 local user = check_credentials(request);
246 if not user then
247 return 401;
248 end
249 -- TODO ask user for consent here
250 if not request.url.query then 632 if not request.url.query then
251 response.headers.content_type = "application/json"; 633 return error_response(request, oauth_error("invalid_request"));
252 return oauth_error("invalid_request");
253 end 634 end
254 local params = http.formdecode(request.url.query); 635 local params = http.formdecode(request.url.query);
255 if not params then 636 if not params then
256 return oauth_error("invalid_request"); 637 return error_response(request, oauth_error("invalid_request"));
257 end 638 end
639
640 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end
641
642 local ok, client = jwt_verify(params.client_id);
643
644 if not ok then
645 return oauth_error("invalid_client", "incorrect credentials");
646 end
647
648 local client_response_types = set.new(array(client.response_types or { "code" }));
649 client_response_types = set.intersection(client_response_types, allowed_response_type_handlers);
650 if not client_response_types:contains(params.response_type) then
651 return oauth_error("invalid_client", "response_type not allowed");
652 end
653
654 local auth_state = get_auth_state(request);
655 if not auth_state.user then
656 -- Render login page
657 return render_page(templates.login, { state = auth_state, client = client });
658 elseif auth_state.consent == nil then
659 -- Render consent page
660 local scopes, requested_roles = split_scopes(parse_scopes(params.scope or ""));
661 local default_role = select_role(auth_state.user.username, requested_roles);
662 local roles = array(it.values(usermanager.get_all_roles(module.host))):filter(function(role)
663 return can_assume_role(auth_state.user.username, role.name);
664 end):sort(function(a, b)
665 return (a.priority or 0) < (b.priority or 0)
666 end):map(function(role)
667 return { name = role.name; selected = role.name == default_role };
668 end);
669 if not roles[2] then
670 -- Only one role to choose from, might as well skip the selector
671 roles = nil;
672 end
673 return render_page(templates.consent, { state = auth_state; client = client; scopes = scopes; roles = roles }, true);
674 elseif not auth_state.consent then
675 -- Notify client of rejection
676 return error_response(request, oauth_error("access_denied"));
677 end
678 -- else auth_state.consent == true
679
680 params.scope = auth_state.scope;
681
682 local user_jid = jid.join(auth_state.user.username, module.host);
683 local client_secret = make_client_secret(params.client_id);
684 local id_token_signer = jwt.new_signer("HS256", client_secret);
685 local id_token = id_token_signer({
686 iss = get_issuer();
687 sub = url.build({ scheme = "xmpp"; path = user_jid });
688 aud = params.client_id;
689 nonce = params.nonce;
690 });
258 local response_type = params.response_type; 691 local response_type = params.response_type;
259 local response_handler = response_type_handlers[response_type]; 692 local response_handler = response_type_handlers[response_type];
260 if not response_handler then 693 if not response_handler then
261 response.headers.content_type = "application/json"; 694 return error_response(request, oauth_error("unsupported_response_type"));
262 return oauth_error("unsupported_response_type"); 695 end
263 end 696 return response_handler(client, params, user_jid, id_token);
264 return response_handler(params, jid.join(user, module.host));
265 end 697 end
266 698
267 local function handle_revocation_request(event) 699 local function handle_revocation_request(event)
268 local request, response = event.request, event.response; 700 local request, response = event.request, event.response;
269 if not request.headers.authorization then 701 if request.headers.authorization then
270 response.headers.www_authenticate = string.format("Basic realm=%q", module.host.."/"..module.name); 702 local credentials = get_request_credentials(request);
271 return 401; 703 if not credentials or credentials.type ~= "basic" then
272 elseif request.headers.content_type ~= "application/x-www-form-urlencoded" 704 response.headers.www_authenticate = string.format("Basic realm=%q", module.host.."/"..module.name);
273 or not request.body or request.body == "" then 705 return 401;
274 return 400; 706 end
275 end 707 -- OAuth "client" credentials
276 local user = check_credentials(request, true); 708 if not verify_client_secret(credentials.username, credentials.password) then
277 if not user then 709 return 401;
278 return 401; 710 end
279 end 711 end
280 712
281 local form_data = http.formdecode(event.request.body); 713 local form_data = http.formdecode(event.request.body or "");
282 if not form_data or not form_data.token then 714 if not form_data or not form_data.token then
283 return 400; 715 response.headers.accept = "application/x-www-form-urlencoded";
716 return 415;
284 end 717 end
285 local ok, err = tokens.revoke_token(form_data.token); 718 local ok, err = tokens.revoke_token(form_data.token);
286 if not ok then 719 if not ok then
287 module:log("warn", "Unable to revoke token: %s", tostring(err)); 720 module:log("warn", "Unable to revoke token: %s", tostring(err));
288 return 500; 721 return 500;
289 end 722 end
290 return 200; 723 return 200;
291 end 724 end
292 725
726 local registration_schema = {
727 type = "object";
728 required = {
729 -- These are shown to users in the template
730 "client_name";
731 "client_uri";
732 -- We need at least one redirect URI for things to work
733 "redirect_uris";
734 };
735 properties = {
736 redirect_uris = { type = "array"; minLength = 1; items = { type = "string"; format = "uri" } };
737 token_endpoint_auth_method = {
738 type = "string";
739 enum = { "none"; "client_secret_post"; "client_secret_basic" };
740 default = "client_secret_basic";
741 };
742 grant_types = {
743 type = "array";
744 items = {
745 type = "string";
746 enum = {
747 "authorization_code";
748 "implicit";
749 "password";
750 "client_credentials";
751 "refresh_token";
752 "urn:ietf:params:oauth:grant-type:jwt-bearer";
753 "urn:ietf:params:oauth:grant-type:saml2-bearer";
754 };
755 };
756 default = { "authorization_code" };
757 };
758 application_type = { type = "string"; enum = { "native"; "web" }; default = "web" };
759 response_types = { type = "array"; items = { type = "string"; enum = { "code"; "token" } }; default = { "code" } };
760 client_name = { type = "string" };
761 client_uri = { type = "string"; format = "uri"; luaPattern = "^https:" };
762 logo_uri = { type = "string"; format = "uri"; luaPattern = "^https:" };
763 scope = { type = "string" };
764 contacts = { type = "array"; items = { type = "string"; format = "email" } };
765 tos_uri = { type = "string"; format = "uri"; luaPattern = "^https:" };
766 policy_uri = { type = "string"; format = "uri"; luaPattern = "^https:" };
767 jwks_uri = { type = "string"; format = "uri"; luaPattern = "^https:" };
768 jwks = { type = "object"; description = "JSON Web Key Set, RFC 7517" };
769 software_id = { type = "string"; format = "uuid" };
770 software_version = { type = "string" };
771 };
772 luaPatternProperties = {
773 -- Localized versions of descriptive properties and URIs
774 ["^client_name#"] = { description = "Localized version of 'client_name'"; type = "string" };
775 ["^[a-z_]+_uri#"] = { type = "string"; format = "uri"; luaPattern = "^https:" };
776 };
777 }
778
779 local function redirect_uri_allowed(redirect_uri, client_uri, app_type)
780 local uri = url.parse(redirect_uri);
781 if app_type == "native" then
782 return uri.scheme == "http" and loopbacks:contains(uri.host) or uri.scheme ~= "https";
783 elseif app_type == "web" then
784 return uri.scheme == "https" and uri.host == client_uri.host;
785 end
786 end
787
788 function create_client(client_metadata)
789 if not schema.validate(registration_schema, client_metadata) then
790 return nil, oauth_error("invalid_request", "Failed schema validation.");
791 end
792
793 -- Fill in default values
794 for propname, propspec in pairs(registration_schema.properties) do
795 if client_metadata[propname] == nil and type(propspec) == "table" and propspec.default ~= nil then
796 client_metadata[propname] = propspec.default;
797 end
798 end
799
800 local client_uri = url.parse(client_metadata.client_uri);
801 if not client_uri or client_uri.scheme ~= "https" or loopbacks:contains(client_uri.host) then
802 return nil, oauth_error("invalid_client_metadata", "Missing, invalid or insecure client_uri");
803 end
804
805 for _, redirect_uri in ipairs(client_metadata.redirect_uris) do
806 if not redirect_uri_allowed(redirect_uri, client_uri, client_metadata.application_type) then
807 return nil, oauth_error("invalid_redirect_uri", "Invalid, insecure or inappropriate redirect URI.");
808 end
809 end
810
811 for field, prop_schema in pairs(registration_schema.properties) do
812 if field ~= "client_uri" and prop_schema.format == "uri" and client_metadata[field] then
813 if not redirect_uri_allowed(client_metadata[field], client_uri, "web") then
814 return nil, oauth_error("invalid_client_metadata", "Invalid, insecure or inappropriate informative URI");
815 end
816 end
817 end
818
819 for k, v in pairs(client_metadata) do
820 local base_k = k:match"^([^#]+)#" or k;
821 if not registration_schema.properties[base_k] or k:find"^client_uri#" then
822 -- Ignore and strip unknown extra properties
823 client_metadata[k] = nil;
824 elseif k:find"_uri#" then
825 -- Localized URIs should be secure too
826 if not redirect_uri_allowed(v, client_uri, "web") then
827 return nil, oauth_error("invalid_client_metadata", "Invalid, insecure or inappropriate informative URI");
828 end
829 end
830 end
831
832 local grant_types = set.new(client_metadata.grant_types);
833 local response_types = set.new(client_metadata.response_types);
834
835 if grant_types:contains("authorization_code") and not response_types:contains("code") then
836 return nil, oauth_error("invalid_client_metadata", "Inconsistency between 'grant_types' and 'response_types'");
837 elseif grant_types:contains("implicit") and not response_types:contains("token") then
838 return nil, oauth_error("invalid_client_metadata", "Inconsistency between 'grant_types' and 'response_types'");
839 end
840
841 if set.intersection(grant_types, allowed_grant_type_handlers):empty() then
842 return nil, oauth_error("invalid_client_metadata", "No allowed 'grant_types' specified");
843 elseif set.intersection(response_types, allowed_response_type_handlers):empty() then
844 return nil, oauth_error("invalid_client_metadata", "No allowed 'response_types' specified");
845 end
846
847 -- Ensure each signed client_id JWT is unique, short ID and issued at
848 -- timestamp should be sufficient to rule out brute force attacks
849 client_metadata.nonce = id.short();
850
851 -- Do we want to keep everything?
852 local client_id = jwt_sign(client_metadata);
853
854 client_metadata.client_id = client_id;
855 client_metadata.client_id_issued_at = os.time();
856
857 if client_metadata.token_endpoint_auth_method ~= "none" then
858 local client_secret = make_client_secret(client_id);
859 client_metadata.client_secret = client_secret;
860 client_metadata.client_secret_expires_at = 0;
861
862 if not registration_options.accept_expired then
863 client_metadata.client_secret_expires_at = client_metadata.client_id_issued_at + (registration_options.default_ttl or 3600);
864 end
865 end
866
867 return client_metadata;
868 end
869
870 local function handle_register_request(event)
871 local request = event.request;
872 local client_metadata, err = json.decode(request.body);
873 if err then
874 return oauth_error("invalid_request", "Invalid JSON");
875 end
876
877 local response, err = create_client(client_metadata);
878 if err then return err end
879
880 return {
881 status_code = 201;
882 headers = { content_type = "application/json" };
883 body = json.encode(response);
884 };
885 end
886
887 if not registration_key then
888 module:log("info", "No 'oauth2_registration_key', dynamic client registration disabled")
889 handle_authorization_request = nil
890 handle_register_request = nil
891 end
892
893 local function handle_userinfo_request(event)
894 local request = event.request;
895 local credentials = get_request_credentials(request);
896 if not credentials or not credentials.bearer_token then
897 module:log("debug", "Missing credentials for UserInfo endpoint: %q", credentials)
898 return 401;
899 end
900 local token_info,err = tokens.get_token_info(credentials.bearer_token);
901 if not token_info then
902 module:log("debug", "UserInfo query failed token validation: %s", err)
903 return 403;
904 end
905 local scopes = set.new()
906 if type(token_info.grant.data) == "table" and type(token_info.grant.data.oauth2_scopes) == "string" then
907 scopes:add_list(parse_scopes(token_info.grant.data.oauth2_scopes));
908 else
909 module:log("debug", "token_info = %q", token_info)
910 end
911
912 if not scopes:contains("openid") then
913 module:log("debug", "Missing the 'openid' scope in %q", scopes)
914 -- The 'openid' scope is required for access to this endpoint.
915 return 403;
916 end
917
918 local user_info = {
919 iss = get_issuer();
920 sub = url.build({ scheme = "xmpp"; path = token_info.jid });
921 }
922
923 local token_claims = set.intersection(openid_claims, scopes);
924 token_claims:remove("openid"); -- that's "iss" and "sub" above
925 if not token_claims:empty() then
926 -- Another module can do that
927 module:fire_event("token/userinfo", {
928 token = token_info;
929 claims = token_claims;
930 username = jid.split(token_info.jid);
931 userinfo = user_info;
932 });
933 end
934
935 return {
936 status_code = 200;
937 headers = { content_type = "application/json" };
938 body = json.encode(user_info);
939 };
940 end
941
293 module:depends("http"); 942 module:depends("http");
294 module:provides("http", { 943 module:provides("http", {
295 route = { 944 route = {
945 -- OAuth 2.0 in 5 simple steps!
946 -- This is the normal 'authorization_code' flow.
947
948 -- Step 1. Create OAuth client
949 ["POST /register"] = handle_register_request;
950
951 -- Step 2. User-facing login and consent view
952 ["GET /authorize"] = handle_authorization_request;
953 ["POST /authorize"] = handle_authorization_request;
954
955 -- Step 3. User is redirected to the 'redirect_uri' along with an
956 -- authorization code. In the insecure 'implicit' flow, the access token
957 -- is delivered here.
958
959 -- Step 4. Retrieve access token using the code.
296 ["POST /token"] = handle_token_grant; 960 ["POST /token"] = handle_token_grant;
297 ["GET /authorize"] = handle_authorization_request; 961
962 -- Step 4 is later repeated using the refresh token to get new access tokens.
963
964 -- Step 5. Revoke token (access or refresh)
298 ["POST /revoke"] = handle_revocation_request; 965 ["POST /revoke"] = handle_revocation_request;
966
967 -- OpenID
968 ["GET /userinfo"] = handle_userinfo_request;
969
970 -- Optional static content for templates
971 ["GET /style.css"] = templates.css and {
972 headers = {
973 ["Content-Type"] = "text/css";
974 };
975 body = _render_html(templates.css, module:get_option("oauth2_template_style"));
976 } or nil;
977 ["GET /script.js"] = templates.js and {
978 headers = {
979 ["Content-Type"] = "text/javascript";
980 };
981 body = templates.js;
982 } or nil;
983
984 -- Some convenient fallback handlers
985 ["GET /register"] = { headers = { content_type = "application/schema+json" }; body = json.encode(registration_schema) };
986 ["GET /token"] = function() return 405; end;
987 ["GET /revoke"] = function() return 405; end;
299 }; 988 };
300 }); 989 });
301 990
302 local http_server = require "net.http.server"; 991 local http_server = require "net.http.server";
303 992
308 end 997 end
309 event.response.headers.content_type = "application/json"; 998 event.response.headers.content_type = "application/json";
310 event.response.status_code = event.error.code or 400; 999 event.response.status_code = event.error.code or 400;
311 return json.encode(oauth2_response); 1000 return json.encode(oauth2_response);
312 end, 5); 1001 end, 5);
1002
1003 -- OIDC Discovery
1004
1005 module:provides("http", {
1006 name = "oauth2-discovery";
1007 default_path = "/.well-known/oauth-authorization-server";
1008 route = {
1009 ["GET"] = {
1010 headers = { content_type = "application/json" };
1011 body = json.encode {
1012 -- RFC 8414: OAuth 2.0 Authorization Server Metadata
1013 issuer = get_issuer();
1014 authorization_endpoint = handle_authorization_request and module:http_url() .. "/authorize" or nil;
1015 token_endpoint = handle_token_grant and module:http_url() .. "/token" or nil;
1016 jwks_uri = nil; -- TODO?
1017 registration_endpoint = handle_register_request and module:http_url() .. "/register" or nil;
1018 scopes_supported = usermanager.get_all_roles and array(it.keys(usermanager.get_all_roles(module.host))):append(array(openid_claims:items()));
1019 response_types_supported = array(it.keys(response_type_handlers));
1020 token_endpoint_auth_methods_supported = array({ "client_secret_post"; "client_secret_basic" });
1021 op_policy_uri = module:get_option_string("oauth2_policy_url", nil);
1022 op_tos_uri = module:get_option_string("oauth2_terms_url", nil);
1023 revocation_endpoint = handle_revocation_request and module:http_url() .. "/revoke" or nil;
1024 revocation_endpoint_auth_methods_supported = array({ "client_secret_basic" });
1025 code_challenge_methods_supported = array(it.keys(verifier_transforms));
1026 grant_types_supported = array(it.keys(response_type_handlers)):map(tmap { token = "implicit"; code = "authorization_code" });
1027 response_modes_supported = array(it.keys(response_type_handlers)):map(tmap { token = "fragment"; code = "query" });
1028 authorization_response_iss_parameter_supported = true;
1029 service_documentation = module:get_option_string("oauth2_service_documentation", "https://modules.prosody.im/mod_http_oauth2.html");
1030
1031 -- OpenID
1032 userinfo_endpoint = handle_register_request and module:http_url() .. "/userinfo" or nil;
1033 id_token_signing_alg_values_supported = { "HS256" };
1034 };
1035 };
1036 };
1037 });
1038
1039 module:shared("tokenauth/oauthbearer_config").oidc_discovery_url = module:http_url("oauth2-discovery", "/.well-known/oauth-authorization-server");