comparison mod_http_oauth2/mod_http_oauth2.lua @ 5208:aaa64c647e12

mod_http_oauth2: Add authentication, consent and error pages
author Matthew Wild <mwild1@gmail.com>
date Mon, 06 Mar 2023 09:46:58 +0000
parents c72e3b0914e8
children 942f8a2f722d
comparison
equal deleted inserted replaced
5207:c72e3b0914e8 5208:aaa64c647e12
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 uuid = require "util.uuid";
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";
12 local schema = require "util.jsonschema"; 13 local schema = require "util.jsonschema";
13 local jwt = require"util.jwt"; 14 local jwt = require"util.jwt";
14 local it = require "util.iterators"; 15 local it = require "util.iterators";
15 local array = require "util.array"; 16 local array = require "util.array";
17 local st = require "util.stanza";
18
19 local function read_file(base_path, fn, required)
20 local f, err = io.open(base_path .. "/" .. fn);
21 if not f then
22 module:log(required and "error" or "debug", "Unable to load template file: %s", err);
23 if required then
24 return error("Failed to load templates");
25 end
26 return nil;
27 end
28 local data = assert(f:read("*a"));
29 assert(f:close());
30 return data;
31 end
32
33 local template_path = module:get_option_path("oauth2_template_path", "html");
34 local templates = {
35 login = read_file(template_path, "login.html", true);
36 consent = read_file(template_path, "consent.html", true);
37 error = read_file(template_path, "error.html", true);
38 css = read_file(template_path, "style.css");
39 js = read_file(template_path, "script.js");
40 };
41
42 local site_name = module:get_option_string("site_name", module.host);
43
44 local _render_html = require"util.interpolation".new("%b{}", st.xml_escape);
45 local function render_page(template, data, sensitive)
46 data = data or {};
47 data.site_name = site_name;
48 local resp = {
49 code = 200;
50 headers = {
51 ["Content-Type"] = "text/html; charset=utf-8";
52 ["Content-Security-Policy"] = "default-src 'self'";
53 ["X-Frame-Options"] = "DENY";
54 ["Cache-Control"] = (sensitive and "no-store" or "no-cache")..", private";
55 };
56 body = _render_html(template, data);
57 };
58 return resp;
59 end
16 60
17 local tokens = module:depends("tokenauth"); 61 local tokens = module:depends("tokenauth");
18 62
19 -- Used to derive client_secret from client_id, set to enable stateless dynamic registration. 63 -- Used to derive client_secret from client_id, set to enable stateless dynamic registration.
20 local registration_key = module:get_option_string("oauth2_registration_key"); 64 local registration_key = module:get_option_string("oauth2_registration_key");
117 local granted_jid = jid.join(request_username, request_host, request_resource); 161 local granted_jid = jid.join(request_username, request_host, request_resource);
118 local granted_scopes = filter_scopes(request_username, request_host, params.scope); 162 local granted_scopes = filter_scopes(request_username, request_host, params.scope);
119 return json.encode(new_access_token(granted_jid, granted_scopes, nil)); 163 return json.encode(new_access_token(granted_jid, granted_scopes, nil));
120 end 164 end
121 165
122 -- TODO response_type_handlers have some common boilerplate code, refactor? 166 function response_type_handlers.code(client, params, granted_jid)
123
124 function response_type_handlers.code(params, granted_jid)
125 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end
126
127 local ok, client = jwt_verify(params.client_id);
128
129 if not ok then
130 return oauth_error("invalid_client", "incorrect credentials");
131 end
132
133 local request_username, request_host = jid.split(granted_jid); 167 local request_username, request_host = jid.split(granted_jid);
134 local granted_scopes = filter_scopes(request_username, request_host, params.scope); 168 local granted_scopes = filter_scopes(request_username, request_host, params.scope);
135 169
136 local code = uuid.generate(); 170 local code = uuid.generate();
137 local ok = codes:set(params.client_id .. "#" .. code, { 171 local ok = codes:set(params.client_id .. "#" .. code, {
176 }; 210 };
177 } 211 }
178 end 212 end
179 213
180 -- Implicit flow 214 -- Implicit flow
181 function response_type_handlers.token(params, granted_jid) 215 function response_type_handlers.token(client, params, granted_jid)
182 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end
183
184 local client = jwt_verify(params.client_id);
185
186 if not client then
187 return oauth_error("invalid_client", "incorrect credentials");
188 end
189
190 local request_username, request_host = jid.split(granted_jid); 216 local request_username, request_host = jid.split(granted_jid);
191 local granted_scopes = filter_scopes(request_username, request_host, params.scope); 217 local granted_scopes = filter_scopes(request_username, request_host, params.scope);
192 local token_info = new_access_token(granted_jid, granted_scopes, nil); 218 local token_info = new_access_token(granted_jid, granted_scopes, nil);
193 219
194 local redirect = url.parse(get_redirect_uri(client, params.redirect_uri)); 220 local redirect = url.parse(get_redirect_uri(client, params.redirect_uri));
234 module:log("debug", "authorization_code invalid or expired: %q", code); 260 module:log("debug", "authorization_code invalid or expired: %q", code);
235 return oauth_error("invalid_client", "incorrect credentials"); 261 return oauth_error("invalid_client", "incorrect credentials");
236 end 262 end
237 263
238 return json.encode(new_access_token(code.granted_jid, code.granted_scopes, nil)); 264 return json.encode(new_access_token(code.granted_jid, code.granted_scopes, nil));
265 end
266
267 -- Used to issue/verify short-lived tokens for the authorization process below
268 local new_user_token, verify_user_token = jwt.init("HS256", random.bytes(32), nil, { default_ttl = 600 });
269
270 -- From the given request, figure out if the user is authenticated and has granted consent yet
271 -- As this requires multiple steps (seek credentials, seek consent), we have a lot of state to
272 -- carry around across requests. We also need to protect against CSRF and session mix-up attacks
273 -- (e.g. the user may have multiple concurrent flows in progress, session cookies aren't unique
274 -- to one of them).
275 -- Our strategy here is to preserve the original query string (containing the authz request), and
276 -- encode the rest of the flow in form POSTs.
277 local function get_auth_state(request)
278 local form = request.method == "POST"
279 and request.body
280 and #request.body > 0
281 and request.headers.content_type == "application/x-www-form-urlencoded"
282 and http.formdecode(request.body);
283
284 if not form then return {}; end
285
286 if not form.user_token then
287 -- First step: login
288 local username = encodings.stringprep.nodeprep(form.username);
289 local password = encodings.stringprep.saslprep(form.password);
290 if not (username and password) or not usermanager.test_password(username, module.host, password) then
291 return {
292 error = "Invalid username/password";
293 };
294 end
295 return {
296 user = {
297 username = username;
298 host = module.host;
299 token = new_user_token({ username = username, host = module.host });
300 };
301 };
302 elseif form.user_token and form.consent then
303 -- Second step: consent
304 local ok, user = verify_user_token(form.user_token);
305 if not ok then
306 return {
307 error = user == "token-expired" and "Session expired - try again" or nil;
308 };
309 end
310
311 user.token = form.user_token;
312 return {
313 user = user;
314 consent = form.consent == "granted";
315 };
316 end
317
318 return {};
239 end 319 end
240 320
241 local function check_credentials(request, allow_token) 321 local function check_credentials(request, allow_token)
242 local auth_type, auth_data = string.match(request.headers.authorization, "^(%S+)%s(.+)$"); 322 local auth_type, auth_data = string.match(request.headers.authorization, "^(%S+)%s(.+)$");
243 323
288 response_type_handlers.token = nil; 368 response_type_handlers.token = nil;
289 grant_type_handlers.authorization_code = nil; 369 grant_type_handlers.authorization_code = nil;
290 check_credentials = function () return false end 370 check_credentials = function () return false end
291 end 371 end
292 372
373 -- OAuth errors should be returned to the client if possible, i.e. by
374 -- appending the error information to the redirect_uri and sending the
375 -- redirect to the user-agent. In some cases we can't do this, e.g. if
376 -- the redirect_uri is missing or invalid. In those cases, we render an
377 -- error directly to the user-agent.
378 local function error_response(request, err)
379 local q = request.url.query and http.formdecode(request.url.query);
380 local redirect_uri = q and q.redirect_uri;
381 if not redirect_uri or not redirect_uri:match("^https://") then
382 module:log("warn", "Missing or invalid redirect_uri <%s>, rendering error to user-agent", redirect_uri or "");
383 return render_page(templates.error, { error = err });
384 end
385 local redirect_query = url.parse(redirect_uri);
386 local sep = redirect_query and "&" or "?";
387 redirect_uri = redirect_uri
388 .. sep .. http.formencode(err.extra.oauth2_response)
389 .. "&" .. http.formencode({ state = q.state, iss = get_issuer() });
390 module:log("warn", "Sending error response to client via redirect to %s", redirect_uri);
391 return {
392 status_code = 302;
393 headers = {
394 location = redirect_uri;
395 };
396 };
397 end
398
293 local allowed_grant_type_handlers = module:get_option_set("allowed_oauth2_grant_types", {"authorization_code", "password"}) 399 local allowed_grant_type_handlers = module:get_option_set("allowed_oauth2_grant_types", {"authorization_code", "password"})
294 for handler_type in pairs(grant_type_handlers) do 400 for handler_type in pairs(grant_type_handlers) do
295 if not allowed_grant_type_handlers:contains(handler_type) then 401 if not allowed_grant_type_handlers:contains(handler_type) then
296 grant_type_handlers[handler_type] = nil; 402 grant_type_handlers[handler_type] = nil;
297 end 403 end
307 413
308 function handle_token_grant(event) 414 function handle_token_grant(event)
309 event.response.headers.content_type = "application/json"; 415 event.response.headers.content_type = "application/json";
310 local params = http.formdecode(event.request.body); 416 local params = http.formdecode(event.request.body);
311 if not params then 417 if not params then
312 return oauth_error("invalid_request"); 418 return error_response(event.request, oauth_error("invalid_request"));
313 end 419 end
314 local grant_type = params.grant_type 420 local grant_type = params.grant_type
315 local grant_handler = grant_type_handlers[grant_type]; 421 local grant_handler = grant_type_handlers[grant_type];
316 if not grant_handler then 422 if not grant_handler then
317 return oauth_error("unsupported_grant_type"); 423 return error_response(event.request, oauth_error("unsupported_grant_type"));
318 end 424 end
319 return grant_handler(params); 425 return grant_handler(params);
320 end 426 end
321 427
322 local function handle_authorization_request(event) 428 local function handle_authorization_request(event)
323 local request, response = event.request, event.response; 429 local request = event.request;
324 if not request.headers.authorization then 430
325 response.headers.www_authenticate = string.format("Basic realm=%q", module.host.."/"..module.name);
326 return 401;
327 end
328 local user = check_credentials(request);
329 if not user then
330 return 401;
331 end
332 -- TODO ask user for consent here
333 if not request.url.query then 431 if not request.url.query then
334 response.headers.content_type = "application/json"; 432 return error_response(request, oauth_error("invalid_request"));
335 return oauth_error("invalid_request");
336 end 433 end
337 local params = http.formdecode(request.url.query); 434 local params = http.formdecode(request.url.query);
338 if not params then 435 if not params then
339 return oauth_error("invalid_request"); 436 return error_response(request, oauth_error("invalid_request"));
340 end 437 end
438
439 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end
440
441 local ok, client = jwt_verify(params.client_id);
442
443 if not ok then
444 return oauth_error("invalid_client", "incorrect credentials");
445 end
446
447 local auth_state = get_auth_state(request);
448 if not auth_state.user then
449 -- Render login page
450 return render_page(templates.login, { state = auth_state, client = client });
451 elseif auth_state.consent == nil then
452 -- Render consent page
453 return render_page(templates.consent, { state = auth_state, client = client }, true);
454 elseif not auth_state.consent then
455 -- Notify client of rejection
456 return error_response(request, oauth_error("access_denied"));
457 end
458
341 local response_type = params.response_type; 459 local response_type = params.response_type;
342 local response_handler = response_type_handlers[response_type]; 460 local response_handler = response_type_handlers[response_type];
343 if not response_handler then 461 if not response_handler then
344 response.headers.content_type = "application/json"; 462 return error_response(request, oauth_error("unsupported_response_type"));
345 return oauth_error("unsupported_response_type"); 463 end
346 end 464 return response_handler(client, params, jid.join(auth_state.user.username, module.host));
347 return response_handler(params, jid.join(user, module.host));
348 end 465 end
349 466
350 local function handle_revocation_request(event) 467 local function handle_revocation_request(event)
351 local request, response = event.request, event.response; 468 local request, response = event.request, event.response;
352 if not request.headers.authorization then 469 if not request.headers.authorization then
450 module:depends("http"); 567 module:depends("http");
451 module:provides("http", { 568 module:provides("http", {
452 route = { 569 route = {
453 ["POST /token"] = handle_token_grant; 570 ["POST /token"] = handle_token_grant;
454 ["GET /authorize"] = handle_authorization_request; 571 ["GET /authorize"] = handle_authorization_request;
572 ["POST /authorize"] = handle_authorization_request;
455 ["POST /revoke"] = handle_revocation_request; 573 ["POST /revoke"] = handle_revocation_request;
456 ["POST /register"] = handle_register_request; 574 ["POST /register"] = handle_register_request;
575
576 -- Optional static content for templates
577 ["GET /style.css"] = templates.css and {
578 headers = {
579 ["Content-Type"] = "text/css";
580 };
581 body = _render_html(templates.css, module:get_option("oauth2_template_style"));
582 } or nil;
583 ["GET /script.js"] = templates.js and {
584 headers = {
585 ["Content-Type"] = "text/javascript";
586 };
587 body = templates.js;
588 } or nil;
457 }; 589 };
458 }); 590 });
459 591
460 local http_server = require "net.http.server"; 592 local http_server = require "net.http.server";
461 593