Mercurial > prosody-modules
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"); |