comparison mod_http_oauth2/mod_http_oauth2.lua @ 4256:c4b9d4ba839b

mod_http_oauth2: Authorization code flow
author Kim Alvefur <zash@zash.se>
date Sat, 21 Nov 2020 01:08:30 +0100
parents a0ab7be0538d
children 145e8e8a247a
comparison
equal deleted inserted replaced
4255:38da10e4b593 4256:c4b9d4ba839b
1 local http = require "util.http"; 1 local http = require "util.http";
2 local jid = require "util.jid"; 2 local jid = require "util.jid";
3 local json = require "util.json"; 3 local json = require "util.json";
4 local usermanager = require "core.usermanager"; 4 local usermanager = require "core.usermanager";
5 local errors = require "util.error"; 5 local errors = require "util.error";
6 local url = require "socket.url";
7 local uuid = require "util.uuid";
8 local encodings = require "util.encodings";
9 local base64 = encodings.base64;
6 10
7 local tokens = module:depends("tokenauth"); 11 local tokens = module:depends("tokenauth");
12
13 local clients = module:open_store("oauth2_clients");
14 local codes = module:open_store("oauth2_codes", "map");
8 15
9 local function oauth_error(err_name, err_desc) 16 local function oauth_error(err_name, err_desc)
10 return errors.new({ 17 return errors.new({
11 type = "modify"; 18 type = "modify";
12 condition = "bad-request"; 19 condition = "bad-request";
25 -- TODO: include refresh_token when implemented 32 -- TODO: include refresh_token when implemented
26 }; 33 };
27 end 34 end
28 35
29 local grant_type_handlers = {}; 36 local grant_type_handlers = {};
37 local response_type_handlers = {};
30 38
31 function grant_type_handlers.password(params) 39 function grant_type_handlers.password(params)
32 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); 40 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)"));
33 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); 41 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'"));
34 local request_username, request_host, request_resource = jid.prepped_split(request_jid); 42 local request_username, request_host, request_resource = jid.prepped_split(request_jid);
43 return json.encode(new_access_token(granted_jid, request_host, nil, nil)); 51 return json.encode(new_access_token(granted_jid, request_host, nil, nil));
44 end 52 end
45 return oauth_error("invalid_grant", "incorrect credentials"); 53 return oauth_error("invalid_grant", "incorrect credentials");
46 end 54 end
47 55
56 function response_type_handlers.code(params, granted_jid)
57 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end
58 if not params.redirect_uri then return oauth_error("invalid_request", "missing 'redirect_uri'"); end
59 if params.scope and params.scope ~= "" then
60 return oauth_error("invalid_scope", "unknown scope requested");
61 end
62
63 local client, err = clients:get(params.client_id);
64 module:log("debug", "clients:get(%q) --> %q, %q", params.client_id, client, err);
65 if err then error(err); end
66 if not client then
67 return oauth_error("invalid_client", "incorrect credentials");
68 end
69
70 local code = uuid.generate();
71 assert(codes:set(params.client_id, code, { issued = os.time(), granted_jid = granted_jid, }));
72
73 local redirect = url.parse(params.redirect_uri);
74 local query = http.formdecode(redirect.query or "");
75 if type(query) ~= "table" then query = {}; end
76 table.insert(query, { name = "code", value = code })
77 if params.state then
78 table.insert(query, { name = "state", value = params.state });
79 end
80 redirect.query = http.formencode(query);
81
82 return {
83 status_code = 302;
84 headers = {
85 location = url.build(redirect);
86 };
87 }
88 end
89
90 function grant_type_handlers.authorization_code(params)
91 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end
92 if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end
93 if not params.code then return oauth_error("invalid_request", "missing 'code'"); end
94 --if not params.redirect_uri then return oauth_error("invalid_request", "missing 'redirect_uri'"); end
95 if params.scope and params.scope ~= "" then
96 return oauth_error("invalid_scope", "unknown scope requested");
97 end
98
99 local client, err = clients:get(params.client_id);
100 if err then error(err); end
101 if not client or client.secret ~= params.client_secret then
102 return oauth_error("invalid_client", "incorrect credentials");
103 end
104 local code, err = codes:get(params.client_id, params.code);
105 if err then error(err); end
106 if not code or type(code) ~= "table" or os.difftime(os.time(), code.issued) > 900 then
107 return oauth_error("invalid_client", "incorrect credentials");
108 end
109 assert(codes:set(params.client_id, params.code, nil));
110
111 if client.redirect_uri and client.redirect_uri ~= params.redirect_uri then
112 return oauth_error("invalid_client", "incorrect 'redirect_uri'");
113 end
114
115 return json.encode(new_access_token(code.granted_jid, nil, nil));
116 end
117
118 local function check_credentials(request)
119 local auth_type, auth_data = string.match(request.headers.authorization, "^(%S+)%s(.+)$");
120
121 if auth_type == "Basic" then
122 local creds = base64.decode(auth_data);
123 if not creds then return false; end
124 local username, password = string.match(creds, "^([^:]+):(.*)$");
125 if not username then return false; end
126 username, password = encodings.stringprep.nodeprep(username), encodings.stringprep.saslprep(password);
127 if not username then return false; end
128 if not usermanager.test_password(username, module.host, password) then
129 return false;
130 end
131 return username;
132 end
133 return nil;
134 end
135
48 if module:get_host_type() == "component" then 136 if module:get_host_type() == "component" then
49 local component_secret = assert(module:get_option_string("component_secret"), "'component_secret' is a required setting when loaded on a Component"); 137 local component_secret = assert(module:get_option_string("component_secret"), "'component_secret' is a required setting when loaded on a Component");
50 138
51 function grant_type_handlers.password(params) 139 function grant_type_handlers.password(params)
52 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); 140 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)"));
62 local granted_jid = jid.join(request_username, request_host, request_resource); 150 local granted_jid = jid.join(request_username, request_host, request_resource);
63 return json.encode(new_access_token(granted_jid, request_host, nil, nil)); 151 return json.encode(new_access_token(granted_jid, request_host, nil, nil));
64 end 152 end
65 return oauth_error("invalid_grant", "incorrect credentials"); 153 return oauth_error("invalid_grant", "incorrect credentials");
66 end 154 end
155
156 -- TODO How would this make sense with components?
157 -- Have an admin authenticate maybe?
158 response_type_handlers.code = nil;
159 grant_type_handlers.authorization_code = nil;
160 check_credentials = function () return false end
67 end 161 end
68 162
69 function handle_token_grant(event) 163 function handle_token_grant(event)
70 event.response.headers.content_type = "application/json"; 164 event.response.headers.content_type = "application/json";
71 local params = http.formdecode(event.request.body); 165 local params = http.formdecode(event.request.body);
78 return oauth_error("unsupported_grant_type"); 172 return oauth_error("unsupported_grant_type");
79 end 173 end
80 return grant_handler(params); 174 return grant_handler(params);
81 end 175 end
82 176
177 local function handle_authorization_request(event)
178 if not event.request.headers.authorization then
179 event.response.headers.www_authenticate = string.format("Basic realm=%q", module.host.."/"..module.name);
180 return 401;
181 end
182 local user = check_credentials(event.request);
183 if not user then
184 return 401;
185 end
186 if not event.request.url.query then
187 event.response.headers.content_type = "application/json";
188 return oauth_error("invalid_request");
189 end
190 local params = http.formdecode(event.request.url.query);
191 if not params then
192 return oauth_error("invalid_request");
193 end
194 local response_type = params.response_type;
195 local response_handler = response_type_handlers[response_type];
196 if not response_handler then
197 event.response.headers.content_type = "application/json";
198 return oauth_error("unsupported_response_type");
199 end
200 return response_handler(params, jid.join(user, module.host));
201 end
202
83 module:depends("http"); 203 module:depends("http");
84 module:provides("http", { 204 module:provides("http", {
85 route = { 205 route = {
86 ["POST /token"] = handle_token_grant; 206 ["POST /token"] = handle_token_grant;
207 ["GET /authorize"] = handle_authorization_request;
87 }; 208 };
88 }); 209 });
89 210
90 local http_server = require "net.http.server"; 211 local http_server = require "net.http.server";
91 212