Mercurial > prosody-modules
comparison mod_http_oauth2/mod_http_oauth2.lua @ 5653:62c6e17a5e9d
Merge
author | Stephen Paul Weber <singpolyma@singpolyma.net> |
---|---|
date | Mon, 18 Sep 2023 08:24:19 -0500 |
parents | d67980d9e12d |
children | bbde136a4c29 |
comparison
equal
deleted
inserted
replaced
5652:eade7ff9f52c | 5653:62c6e17a5e9d |
---|---|
1 local usermanager = require "core.usermanager"; | |
2 local url = require "socket.url"; | |
3 local array = require "util.array"; | |
4 local cache = require "util.cache"; | |
5 local encodings = require "util.encodings"; | |
6 local errors = require "util.error"; | |
1 local hashes = require "util.hashes"; | 7 local hashes = require "util.hashes"; |
2 local cache = require "util.cache"; | |
3 local http = require "util.http"; | 8 local http = require "util.http"; |
9 local id = require "util.id"; | |
10 local it = require "util.iterators"; | |
4 local jid = require "util.jid"; | 11 local jid = require "util.jid"; |
5 local json = require "util.json"; | 12 local json = require "util.json"; |
6 local usermanager = require "core.usermanager"; | 13 local schema = require "util.jsonschema"; |
7 local errors = require "util.error"; | 14 local jwt = require "util.jwt"; |
8 local url = require "socket.url"; | 15 local random = require "util.random"; |
9 local id = require "util.id"; | 16 local set = require "util.set"; |
10 local encodings = require "util.encodings"; | 17 local st = require "util.stanza"; |
18 | |
11 local base64 = encodings.base64; | 19 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 |
20 local function b64url(s) | 21 local function b64url(s) |
21 return (base64.encode(s):gsub("[+/=]", { ["+"] = "-", ["/"] = "_", ["="] = "" })) | 22 return (base64.encode(s):gsub("[+/=]", { ["+"] = "-", ["/"] = "_", ["="] = "" })) |
22 end | 23 end |
23 | 24 |
24 local function tmap(t) | 25 local function tmap(t) |
25 return function(k) | 26 return function(k) |
26 return t[k]; | 27 return t[k]; |
27 end | 28 end |
29 end | |
30 | |
31 local function strict_formdecode(query) | |
32 if not query then | |
33 return nil; | |
34 end | |
35 local params = http.formdecode(query); | |
36 if type(params) ~= "table" then | |
37 return nil, "no-pairs"; | |
38 end | |
39 local dups = {}; | |
40 for _, pair in ipairs(params) do | |
41 if dups[pair.name] then | |
42 return nil, "duplicate"; | |
43 end | |
44 dups[pair.name] = true; | |
45 end | |
46 return params; | |
28 end | 47 end |
29 | 48 |
30 local function read_file(base_path, fn, required) | 49 local function read_file(base_path, fn, required) |
31 local f, err = io.open(base_path .. "/" .. fn); | 50 local f, err = io.open(base_path .. "/" .. fn); |
32 if not f then | 51 if not f then |
39 local data = assert(f:read("*a")); | 58 local data = assert(f:read("*a")); |
40 assert(f:close()); | 59 assert(f:close()); |
41 return data; | 60 return data; |
42 end | 61 end |
43 | 62 |
63 local allowed_locales = module:get_option_array("allowed_oauth2_locales", {}); | |
64 -- TODO Allow translations or per-locale templates somehow. | |
65 | |
44 local template_path = module:get_option_path("oauth2_template_path", "html"); | 66 local template_path = module:get_option_path("oauth2_template_path", "html"); |
45 local templates = { | 67 local templates = { |
46 login = read_file(template_path, "login.html", true); | 68 login = read_file(template_path, "login.html", true); |
47 consent = read_file(template_path, "consent.html", true); | 69 consent = read_file(template_path, "consent.html", true); |
70 oob = read_file(template_path, "oob.html", true); | |
71 device = read_file(template_path, "device.html", true); | |
48 error = read_file(template_path, "error.html", true); | 72 error = read_file(template_path, "error.html", true); |
49 css = read_file(template_path, "style.css"); | 73 css = read_file(template_path, "style.css"); |
50 js = read_file(template_path, "script.js"); | 74 js = read_file(template_path, "script.js"); |
51 }; | 75 }; |
52 | 76 |
53 local site_name = module:get_option_string("site_name", module.host); | 77 local site_name = module:get_option_string("site_name", module.host); |
54 | 78 |
55 local _render_html = require"util.interpolation".new("%b{}", st.xml_escape); | 79 local security_policy = module:get_option_string("oauth2_security_policy", "default-src 'self'"); |
80 | |
81 local render_html = require"util.interpolation".new("%b{}", st.xml_escape); | |
56 local function render_page(template, data, sensitive) | 82 local function render_page(template, data, sensitive) |
57 data = data or {}; | 83 data = data or {}; |
58 data.site_name = site_name; | 84 data.site_name = site_name; |
59 local resp = { | 85 local resp = { |
60 status_code = 200; | 86 status_code = data.error and data.error.code or 200; |
61 headers = { | 87 headers = { |
62 ["Content-Type"] = "text/html; charset=utf-8"; | 88 ["Content-Type"] = "text/html; charset=utf-8"; |
63 ["Content-Security-Policy"] = "default-src 'self'"; | 89 ["Content-Security-Policy"] = security_policy; |
90 ["Referrer-Policy"] = "no-referrer"; | |
64 ["X-Frame-Options"] = "DENY"; | 91 ["X-Frame-Options"] = "DENY"; |
65 ["Cache-Control"] = (sensitive and "no-store" or "no-cache")..", private"; | 92 ["Cache-Control"] = (sensitive and "no-store" or "no-cache")..", private"; |
66 }; | 93 ["Pragma"] = "no-cache"; |
67 body = _render_html(template, data); | 94 }; |
95 body = render_html(template, data); | |
68 }; | 96 }; |
69 return resp; | 97 return resp; |
70 end | 98 end |
71 | 99 |
100 local authorization_server_metadata = nil; | |
101 | |
72 local tokens = module:depends("tokenauth"); | 102 local tokens = module:depends("tokenauth"); |
73 | 103 |
74 local default_access_ttl = module:get_option_number("oauth2_access_token_ttl", 86400); | 104 local default_access_ttl = module:get_option_number("oauth2_access_token_ttl", 3600); |
75 local default_refresh_ttl = module:get_option_number("oauth2_refresh_token_ttl", nil); | 105 local default_refresh_ttl = module:get_option_number("oauth2_refresh_token_ttl", 604800); |
76 | 106 |
77 -- Used to derive client_secret from client_id, set to enable stateless dynamic registration. | 107 -- Used to derive client_secret from client_id, set to enable stateless dynamic registration. |
78 local registration_key = module:get_option_string("oauth2_registration_key"); | 108 local registration_key = module:get_option_string("oauth2_registration_key"); |
79 local registration_algo = module:get_option_string("oauth2_registration_algorithm", "HS256"); | 109 local registration_algo = module:get_option_string("oauth2_registration_algorithm", "HS256"); |
80 local registration_ttl = module:get_option("oauth2_registration_ttl", nil); | 110 local registration_ttl = module:get_option("oauth2_registration_ttl", nil); |
82 { default_ttl = registration_ttl; accept_expired = not registration_ttl }); | 112 { default_ttl = registration_ttl; accept_expired = not registration_ttl }); |
83 | 113 |
84 local pkce_required = module:get_option_boolean("oauth2_require_code_challenge", false); | 114 local pkce_required = module:get_option_boolean("oauth2_require_code_challenge", false); |
85 | 115 |
86 local verification_key; | 116 local verification_key; |
87 local jwt_sign, jwt_verify; | 117 local sign_client, verify_client; |
88 if registration_key then | 118 if registration_key then |
89 -- Tie it to the host if global | 119 -- Tie it to the host if global |
90 verification_key = hashes.hmac_sha256(registration_key, module.host); | 120 verification_key = hashes.hmac_sha256(registration_key, module.host); |
91 jwt_sign, jwt_verify = jwt.init(registration_algo, registration_key, registration_key, registration_options); | 121 sign_client, verify_client = jwt.init(registration_algo, registration_key, registration_key, registration_options); |
92 end | 122 end |
93 | 123 |
124 local new_device_token, verify_device_token = jwt.init("HS256", random.bytes(32), nil, { default_ttl = 600 }); | |
125 | |
126 -- verify and prepare client structure | |
127 local function check_client(client_id) | |
128 if not verify_client then | |
129 return nil, "client-registration-not-enabled"; | |
130 end | |
131 | |
132 local ok, client = verify_client(client_id); | |
133 if not ok then | |
134 return ok, client; | |
135 end | |
136 | |
137 client.client_hash = b64url(hashes.sha256(client_id)); | |
138 return client; | |
139 end | |
140 | |
141 -- scope : string | array | set | |
142 -- | |
143 -- at each step, allow the same or a subset of scopes | |
144 -- (all ( client ( grant ( token ) ) )) | |
145 -- preserve order since it determines role if more than one granted | |
146 | |
147 -- string -> array | |
94 local function parse_scopes(scope_string) | 148 local function parse_scopes(scope_string) |
95 return array(scope_string:gmatch("%S+")); | 149 return array(scope_string:gmatch("%S+")); |
96 end | 150 end |
97 | 151 |
98 local openid_claims = set.new({ "openid", "profile"; "email"; "address"; "phone" }); | 152 local openid_claims = set.new(); |
99 | 153 module:add_item("openid-claim", "openid"); |
154 | |
155 module:handle_items("openid-claim", function(event) | |
156 authorization_server_metadata = nil; | |
157 openid_claims:add(event.item); | |
158 end, function() | |
159 authorization_server_metadata = nil; | |
160 openid_claims = set.new(module:get_host_items("openid-claim")); | |
161 end, true); | |
162 | |
163 -- array -> array, array, array | |
100 local function split_scopes(scope_list) | 164 local function split_scopes(scope_list) |
101 local claims, roles, unknown = array(), array(), array(); | 165 local claims, roles, unknown = array(), array(), array(); |
102 local all_roles = usermanager.get_all_roles(module.host); | 166 local all_roles = usermanager.get_all_roles(module.host); |
103 for _, scope in ipairs(scope_list) do | 167 for _, scope in ipairs(scope_list) do |
104 if openid_claims:contains(scope) then | 168 if openid_claims:contains(scope) then |
105 claims:push(scope); | 169 claims:push(scope); |
106 elseif all_roles[scope] then | 170 elseif scope == "xmpp" or all_roles[scope] then |
107 roles:push(scope); | 171 roles:push(scope); |
108 else | 172 else |
109 unknown:push(scope); | 173 unknown:push(scope); |
110 end | 174 end |
111 end | 175 end |
112 return claims, roles, unknown; | 176 return claims, roles, unknown; |
113 end | 177 end |
114 | 178 |
115 local function can_assume_role(username, requested_role) | 179 local function can_assume_role(username, requested_role) |
116 return usermanager.user_can_assume_role(username, module.host, requested_role); | 180 return requested_role == "xmpp" or usermanager.user_can_assume_role(username, module.host, requested_role); |
117 end | 181 end |
118 | 182 |
119 local function select_role(username, requested_roles) | 183 -- function (string) : function(string) : boolean |
120 if requested_roles then | 184 local function role_assumable_by(username) |
121 for _, requested_role in ipairs(requested_roles) do | 185 return function(role) |
122 if can_assume_role(username, requested_role) then | 186 return can_assume_role(username, role); |
123 return requested_role; | 187 end |
124 end | 188 end |
125 end | 189 |
126 end | 190 -- string, array --> array |
127 -- otherwise the default role | 191 local function user_assumable_roles(username, requested_roles) |
128 return usermanager.get_user_role(username, module.host).name; | 192 return array.filter(requested_roles, role_assumable_by(username)); |
129 end | 193 end |
130 | 194 |
195 -- string, string|nil --> string, string | |
131 local function filter_scopes(username, requested_scope_string) | 196 local function filter_scopes(username, requested_scope_string) |
132 local granted_scopes, requested_roles; | 197 local requested_scopes, requested_roles = split_scopes(parse_scopes(requested_scope_string or "")); |
133 | 198 |
134 if requested_scope_string then -- Specific role(s) requested | 199 local granted_roles = user_assumable_roles(username, requested_roles); |
135 granted_scopes, requested_roles = split_scopes(parse_scopes(requested_scope_string)); | 200 local granted_scopes = requested_scopes + granted_roles; |
136 else | 201 |
137 granted_scopes = array(); | 202 local selected_role = granted_roles[1]; |
138 end | |
139 | |
140 local selected_role = select_role(username, requested_roles); | |
141 granted_scopes:push(selected_role); | |
142 | 203 |
143 return granted_scopes:concat(" "), selected_role; | 204 return granted_scopes:concat(" "), selected_role; |
144 end | 205 end |
145 | 206 |
146 local function code_expires_in(code) --> number, seconds until code expires | 207 local function code_expires_in(code) --> number, seconds until code expires |
153 | 214 |
154 local codes = cache.new(10000, function (_, code) | 215 local codes = cache.new(10000, function (_, code) |
155 return code_expired(code) | 216 return code_expired(code) |
156 end); | 217 end); |
157 | 218 |
158 -- Periodically clear out unredeemed codes. Does not need to be exact, expired | 219 -- Clear out unredeemed codes so they don't linger in memory. |
159 -- codes are rejected if tried. Mostly just to keep memory usage in check. | 220 module:daily("Clear expired authorization codes", function() |
160 module:hourly("Clear expired authorization codes", function() | |
161 local k, code = codes:tail(); | 221 local k, code = codes:tail(); |
162 while code and code_expired(code) do | 222 while code and code_expired(code) do |
163 codes:set(k, nil); | 223 codes:set(k, nil); |
164 k, code = codes:tail(); | 224 k, code = codes:tail(); |
165 end | 225 end |
167 | 227 |
168 local function get_issuer() | 228 local function get_issuer() |
169 return (module:http_url(nil, "/"):gsub("/$", "")); | 229 return (module:http_url(nil, "/"):gsub("/$", "")); |
170 end | 230 end |
171 | 231 |
232 -- Non-standard special redirect URI that has the AS show the authorization | |
233 -- code to the user for them to copy-paste into the client, which can then | |
234 -- continue as if it received it via redirect. | |
235 local oob_uri = "urn:ietf:wg:oauth:2.0:oob"; | |
236 local device_uri = "urn:ietf:params:oauth:grant-type:device_code"; | |
237 | |
172 local loopbacks = set.new({ "localhost", "127.0.0.1", "::1" }); | 238 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 | |
177 | 239 |
178 local function oauth_error(err_name, err_desc) | 240 local function oauth_error(err_name, err_desc) |
179 return errors.new({ | 241 return errors.new({ |
180 type = "modify"; | 242 type = "modify"; |
181 condition = "bad-request"; | 243 condition = "bad-request"; |
187 | 249 |
188 -- client_id / client_metadata are pretty large, filter out a subset of | 250 -- client_id / client_metadata are pretty large, filter out a subset of |
189 -- properties that are deemed useful e.g. in case tokens issued to a certain | 251 -- properties that are deemed useful e.g. in case tokens issued to a certain |
190 -- client needs to be revoked | 252 -- client needs to be revoked |
191 local function client_subset(client) | 253 local function client_subset(client) |
192 return { name = client.client_name; uri = client.client_uri; id = client.software_id; version = client.software_version }; | 254 return { |
255 name = client.client_name; | |
256 uri = client.client_uri; | |
257 id = client.software_id; | |
258 version = client.software_version; | |
259 hash = client.client_hash; | |
260 }; | |
193 end | 261 end |
194 | 262 |
195 local function new_access_token(token_jid, role, scope_string, client, id_token, refresh_token_info) | 263 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 }; | 264 local token_data = { oauth2_scopes = scope_string, oauth2_client = nil }; |
197 if client then | 265 if client then |
199 end | 267 end |
200 if next(token_data) == nil then | 268 if next(token_data) == nil then |
201 token_data = nil; | 269 token_data = nil; |
202 end | 270 end |
203 | 271 |
204 local refresh_token; | |
205 local grant = refresh_token_info and refresh_token_info.grant; | 272 local grant = refresh_token_info and refresh_token_info.grant; |
206 if not grant then | 273 if not grant then |
207 -- No existing grant, create one | 274 -- No existing grant, create one |
208 grant = tokens.create_grant(token_jid, token_jid, default_refresh_ttl, token_data); | 275 grant = tokens.create_grant(token_jid, token_jid, nil, token_data); |
209 -- Create refresh token for the grant if desired | 276 end |
210 refresh_token = refresh_token_info ~= false and tokens.create_token(token_jid, grant, nil, nil, "oauth2-refresh"); | 277 |
211 else | 278 if refresh_token_info then |
212 -- Grant exists, reuse existing refresh token | 279 -- out with the old refresh tokens |
213 refresh_token = refresh_token_info.token; | 280 local ok, err = tokens.revoke_token(refresh_token_info.token); |
214 | 281 if not ok then |
215 refresh_token_info.grant = nil; -- Prevent reference loop | 282 module:log("error", "Could not revoke refresh token: %s", err); |
216 end | 283 return 500; |
217 | 284 end |
218 local access_token, access_token_info = tokens.create_token(token_jid, grant, role, default_access_ttl, "oauth2"); | 285 end |
286 -- in with the new refresh token | |
287 local refresh_token = refresh_token_info ~= false and tokens.create_token(token_jid, grant.id, nil, default_refresh_ttl, "oauth2-refresh"); | |
288 | |
289 if role == "xmpp" then | |
290 -- Special scope meaning the users default role. | |
291 local user_default_role = usermanager.get_user_role(jid.node(token_jid), module.host); | |
292 role = user_default_role and user_default_role.name; | |
293 end | |
294 | |
295 local access_token, access_token_info = tokens.create_token(token_jid, grant.id, role, default_access_ttl, "oauth2"); | |
219 | 296 |
220 local expires_at = access_token_info.expires; | 297 local expires_at = access_token_info.expires; |
221 return { | 298 return { |
222 token_type = "bearer"; | 299 token_type = "bearer"; |
223 access_token = access_token; | 300 access_token = access_token; |
226 id_token = id_token; | 303 id_token = id_token; |
227 refresh_token = refresh_token or nil; | 304 refresh_token = refresh_token or nil; |
228 }; | 305 }; |
229 end | 306 end |
230 | 307 |
308 local function normalize_loopback(uri) | |
309 local u = url.parse(uri); | |
310 if u.scheme == "http" and loopbacks:contains(u.host) then | |
311 u.authority = nil; | |
312 u.host = "::1"; | |
313 u.port = nil; | |
314 return url.build(u); | |
315 end | |
316 -- else, not a valid loopback uri | |
317 end | |
318 | |
231 local function get_redirect_uri(client, query_redirect_uri) -- record client, string : string | 319 local function get_redirect_uri(client, query_redirect_uri) -- record client, string : string |
232 if not query_redirect_uri then | 320 if not query_redirect_uri then |
233 if #client.redirect_uris ~= 1 then | 321 if #client.redirect_uris ~= 1 then |
234 -- Client registered multiple URIs, it needs specify which one to use | 322 -- Client registered multiple URIs, it needs specify which one to use |
235 return; | 323 return; |
236 end | 324 end |
237 -- When only a single URI is registered, that's the default | 325 -- When only a single URI is registered, that's the default |
238 return client.redirect_uris[1]; | 326 return client.redirect_uris[1]; |
239 end | 327 end |
328 if query_redirect_uri == device_uri and client.grant_types then | |
329 for _, grant_type in ipairs(client.grant_types) do | |
330 if grant_type == device_uri then | |
331 return query_redirect_uri; | |
332 end | |
333 end | |
334 -- Tried to use device authorization flow without registering it. | |
335 return; | |
336 end | |
240 -- Verify the client-provided URI matches one previously registered | 337 -- Verify the client-provided URI matches one previously registered |
241 for _, redirect_uri in ipairs(client.redirect_uris) do | 338 for _, redirect_uri in ipairs(client.redirect_uris) do |
242 if query_redirect_uri == redirect_uri then | 339 if query_redirect_uri == redirect_uri then |
243 return redirect_uri | 340 return redirect_uri |
244 end | 341 end |
245 end | 342 end |
343 -- The authorization server MUST allow any port to be specified at the time | |
344 -- of the request for loopback IP redirect URIs, to accommodate clients that | |
345 -- obtain an available ephemeral port from the operating system at the time | |
346 -- of the request. | |
347 -- https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-08.html#section-8.4.2 | |
348 local loopback_redirect_uri = normalize_loopback(query_redirect_uri); | |
349 if loopback_redirect_uri then | |
350 for _, redirect_uri in ipairs(client.redirect_uris) do | |
351 if loopback_redirect_uri == normalize_loopback(redirect_uri) then | |
352 return query_redirect_uri; | |
353 end | |
354 end | |
355 end | |
246 end | 356 end |
247 | 357 |
248 local grant_type_handlers = {}; | 358 local grant_type_handlers = {}; |
249 local response_type_handlers = {}; | 359 local response_type_handlers = {}; |
250 local verifier_transforms = {}; | 360 local verifier_transforms = {}; |
361 | |
362 function grant_type_handlers.implicit() | |
363 -- Placeholder to make discovery work correctly. | |
364 -- Access tokens are delivered via redirect when using the implict flow, not | |
365 -- via the token endpoint, so how did you get here? | |
366 return oauth_error("invalid_request"); | |
367 end | |
251 | 368 |
252 function grant_type_handlers.password(params) | 369 function grant_type_handlers.password(params) |
253 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); | 370 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); |
254 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); | 371 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); |
255 local request_username, request_host, request_resource = jid.prepped_split(request_jid); | 372 local request_username, request_host, request_resource = jid.prepped_split(request_jid); |
275 | 392 |
276 if pkce_required and not params.code_challenge then | 393 if pkce_required and not params.code_challenge then |
277 return oauth_error("invalid_request", "PKCE required"); | 394 return oauth_error("invalid_request", "PKCE required"); |
278 end | 395 end |
279 | 396 |
397 local prefix = "authorization_code:"; | |
280 local code = id.medium(); | 398 local code = id.medium(); |
281 local ok = codes:set(params.client_id .. "#" .. code, { | 399 if params.redirect_uri == device_uri then |
400 local is_device, device_state = verify_device_token(params.state); | |
401 if is_device then | |
402 -- reconstruct the device_code | |
403 prefix = "device_code:"; | |
404 code = b64url(hashes.hmac_sha256(verification_key, device_state.user_code)); | |
405 else | |
406 return oauth_error("invalid_request"); | |
407 end | |
408 end | |
409 local ok = codes:set(prefix.. params.client_id .. "#" .. code, { | |
282 expires = os.time() + 600; | 410 expires = os.time() + 600; |
283 granted_jid = granted_jid; | 411 granted_jid = granted_jid; |
284 granted_scopes = granted_scopes; | 412 granted_scopes = granted_scopes; |
285 granted_role = granted_role; | 413 granted_role = granted_role; |
286 challenge = params.code_challenge; | 414 challenge = params.code_challenge; |
287 challenge_method = params.code_challenge_method; | 415 challenge_method = params.code_challenge_method; |
288 id_token = id_token; | 416 id_token = id_token; |
289 }); | 417 }); |
290 if not ok then | 418 if not ok then |
291 return {status_code = 429}; | 419 return oauth_error("temporarily_unavailable"); |
292 end | 420 end |
293 | 421 |
294 local redirect_uri = get_redirect_uri(client, params.redirect_uri); | 422 local redirect_uri = get_redirect_uri(client, params.redirect_uri); |
295 if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" then | 423 if redirect_uri == oob_uri then |
296 -- TODO some nicer template page | 424 return render_page(templates.oob, { client = client; authorization_code = code }, true); |
297 -- mod_http_errors will set content-type to text/html if it catches this | 425 elseif redirect_uri == device_uri then |
298 -- event, if not text/plain is kept for the fallback text. | 426 return render_page(templates.device, { client = client }, true); |
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 | 427 elseif not redirect_uri then |
308 return 400; | 428 return oauth_error("invalid_redirect_uri"); |
309 end | 429 end |
310 | 430 |
311 local redirect = url.parse(redirect_uri); | 431 local redirect = url.parse(redirect_uri); |
312 | 432 |
313 local query = http.formdecode(redirect.query or ""); | 433 local query = strict_formdecode(redirect.query); |
314 if type(query) ~= "table" then query = {}; end | 434 if type(query) ~= "table" then query = {}; end |
315 table.insert(query, { name = "code", value = code }); | 435 table.insert(query, { name = "code", value = code }); |
316 table.insert(query, { name = "iss", value = get_issuer() }); | 436 table.insert(query, { name = "iss", value = get_issuer() }); |
317 if params.state then | 437 if params.state then |
318 table.insert(query, { name = "state", value = params.state }); | 438 table.insert(query, { name = "state", value = params.state }); |
320 redirect.query = http.formencode(query); | 440 redirect.query = http.formencode(query); |
321 | 441 |
322 return { | 442 return { |
323 status_code = 303; | 443 status_code = 303; |
324 headers = { | 444 headers = { |
445 cache_control = "no-store"; | |
446 pragma = "no-cache"; | |
325 location = url.build(redirect); | 447 location = url.build(redirect); |
326 }; | 448 }; |
327 } | 449 } |
328 end | 450 end |
329 | 451 |
335 end | 457 end |
336 local granted_scopes, granted_role = filter_scopes(request_username, params.scope); | 458 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); | 459 local token_info = new_access_token(granted_jid, granted_role, granted_scopes, client, nil); |
338 | 460 |
339 local redirect = url.parse(get_redirect_uri(client, params.redirect_uri)); | 461 local redirect = url.parse(get_redirect_uri(client, params.redirect_uri)); |
340 if not redirect then return 400; end | 462 if not redirect then return oauth_error("invalid_redirect_uri"); end |
341 token_info.state = params.state; | 463 token_info.state = params.state; |
342 redirect.fragment = http.formencode(token_info); | 464 redirect.fragment = http.formencode(token_info); |
343 | 465 |
344 return { | 466 return { |
345 status_code = 303; | 467 status_code = 303; |
346 headers = { | 468 headers = { |
469 cache_control = "no-store"; | |
470 pragma = "no-cache"; | |
347 location = url.build(redirect); | 471 location = url.build(redirect); |
348 }; | 472 }; |
349 } | 473 } |
350 end | 474 end |
351 | 475 |
360 function grant_type_handlers.authorization_code(params) | 484 function grant_type_handlers.authorization_code(params) |
361 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end | 485 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end |
362 if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end | 486 if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end |
363 if not params.code then return oauth_error("invalid_request", "missing 'code'"); end | 487 if not params.code then return oauth_error("invalid_request", "missing 'code'"); end |
364 if params.scope and params.scope ~= "" then | 488 if params.scope and params.scope ~= "" then |
489 -- FIXME allow a subset of granted scopes | |
365 return oauth_error("invalid_scope", "unknown scope requested"); | 490 return oauth_error("invalid_scope", "unknown scope requested"); |
366 end | 491 end |
367 | 492 |
368 local client_ok, client = jwt_verify(params.client_id); | 493 local client = check_client(params.client_id); |
369 if not client_ok then | 494 if not client then |
370 return oauth_error("invalid_client", "incorrect credentials"); | 495 return oauth_error("invalid_client", "incorrect credentials"); |
371 end | 496 end |
372 | 497 |
373 if not verify_client_secret(params.client_id, params.client_secret) then | 498 if not verify_client_secret(params.client_id, params.client_secret) then |
374 module:log("debug", "client_secret mismatch"); | 499 module:log("debug", "client_secret mismatch"); |
375 return oauth_error("invalid_client", "incorrect credentials"); | 500 return oauth_error("invalid_client", "incorrect credentials"); |
376 end | 501 end |
377 local code, err = codes:get(params.client_id .. "#" .. params.code); | 502 local code, err = codes:get("authorization_code:" .. params.client_id .. "#" .. params.code); |
378 if err then error(err); end | 503 if err then error(err); end |
379 -- MUST NOT use the authorization code more than once, so remove it to | 504 -- MUST NOT use the authorization code more than once, so remove it to |
380 -- prevent a second attempted use | 505 -- prevent a second attempted use |
381 codes:set(params.client_id .. "#" .. params.code, nil); | 506 -- TODO if a second attempt *is* made, revoke any tokens issued |
507 codes:set("authorization_code:" .. params.client_id .. "#" .. params.code, nil); | |
382 if not code or type(code) ~= "table" or code_expired(code) then | 508 if not code or type(code) ~= "table" or code_expired(code) then |
383 module:log("debug", "authorization_code invalid or expired: %q", code); | 509 module:log("debug", "authorization_code invalid or expired: %q", code); |
384 return oauth_error("invalid_client", "incorrect credentials"); | 510 return oauth_error("invalid_client", "incorrect credentials"); |
385 end | 511 end |
386 | 512 |
398 function grant_type_handlers.refresh_token(params) | 524 function grant_type_handlers.refresh_token(params) |
399 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end | 525 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 | 526 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 | 527 if not params.refresh_token then return oauth_error("invalid_request", "missing 'refresh_token'"); end |
402 | 528 |
403 local client_ok, client = jwt_verify(params.client_id); | 529 local client = check_client(params.client_id); |
404 if not client_ok then | 530 if not client then |
405 return oauth_error("invalid_client", "incorrect credentials"); | 531 return oauth_error("invalid_client", "incorrect credentials"); |
406 end | 532 end |
407 | 533 |
408 if not verify_client_secret(params.client_id, params.client_secret) then | 534 if not verify_client_secret(params.client_id, params.client_secret) then |
409 module:log("debug", "client_secret mismatch"); | 535 module:log("debug", "client_secret mismatch"); |
413 local refresh_token_info = tokens.get_token_info(params.refresh_token); | 539 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 | 540 if not refresh_token_info or refresh_token_info.purpose ~= "oauth2-refresh" then |
415 return oauth_error("invalid_grant", "invalid refresh token"); | 541 return oauth_error("invalid_grant", "invalid refresh token"); |
416 end | 542 end |
417 | 543 |
544 local refresh_token_client = refresh_token_info.grant.data.oauth2_client; | |
545 if not refresh_token_client.hash or refresh_token_client.hash ~= client.client_hash then | |
546 module:log("warn", "OAuth client %q (%s) tried to use refresh token belonging to %q (%s)", client.client_name, client.client_hash, | |
547 refresh_token_client.name, refresh_token_client.hash); | |
548 return oauth_error("unauthorized_client", "incorrect credentials"); | |
549 end | |
550 | |
551 local refresh_scopes = refresh_token_info.grant.data.oauth2_scopes; | |
552 | |
553 if params.scope then | |
554 local granted_scopes = set.new(parse_scopes(refresh_scopes)); | |
555 local requested_scopes = parse_scopes(params.scope); | |
556 refresh_scopes = array.filter(requested_scopes, function(scope) | |
557 return granted_scopes:contains(scope); | |
558 end):concat(" "); | |
559 end | |
560 | |
561 local username = jid.split(refresh_token_info.jid); | |
562 local new_scopes, role = filter_scopes(username, refresh_scopes); | |
563 | |
418 -- new_access_token() requires the actual token | 564 -- new_access_token() requires the actual token |
419 refresh_token_info.token = params.refresh_token; | 565 refresh_token_info.token = params.refresh_token; |
420 | 566 |
421 return json.encode(new_access_token( | 567 return json.encode(new_access_token(refresh_token_info.jid, role, new_scopes, client, nil, refresh_token_info)); |
422 refresh_token_info.jid, refresh_token_info.role, refresh_token_info.grant.data.oauth2_scopes, client, nil, refresh_token_info | 568 end |
423 )); | 569 |
570 grant_type_handlers[device_uri] = function(params) | |
571 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end | |
572 if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end | |
573 if not params.device_code then return oauth_error("invalid_request", "missing 'device_code'"); end | |
574 | |
575 local client = check_client(params.client_id); | |
576 if not client then | |
577 return oauth_error("invalid_client", "incorrect credentials"); | |
578 end | |
579 | |
580 if not verify_client_secret(params.client_id, params.client_secret) then | |
581 module:log("debug", "client_secret mismatch"); | |
582 return oauth_error("invalid_client", "incorrect credentials"); | |
583 end | |
584 | |
585 local code = codes:get("device_code:" .. params.client_id .. "#" .. params.device_code); | |
586 if type(code) ~= "table" or code_expired(code) then | |
587 return oauth_error("expired_token"); | |
588 elseif code.error then | |
589 return code.error; | |
590 elseif not code.granted_jid then | |
591 return oauth_error("authorization_pending"); | |
592 end | |
593 codes:set("device_code:" .. params.client_id .. "#" .. params.device_code, nil); | |
594 | |
595 return json.encode(new_access_token(code.granted_jid, code.granted_role, code.granted_scopes, client, code.id_token)); | |
424 end | 596 end |
425 | 597 |
426 -- RFC 7636 Proof Key for Code Exchange by OAuth Public Clients | 598 -- RFC 7636 Proof Key for Code Exchange by OAuth Public Clients |
427 | 599 |
428 function verifier_transforms.plain(code_verifier) | 600 function verifier_transforms.plain(code_verifier) |
465 end | 637 end |
466 return { | 638 return { |
467 user = { | 639 user = { |
468 username = username; | 640 username = username; |
469 host = module.host; | 641 host = module.host; |
470 token = new_user_token({ username = username, host = module.host }); | 642 token = new_user_token({ username = username; host = module.host; auth_time = os.time() }); |
471 }; | 643 }; |
472 }; | 644 }; |
473 elseif form.user_token and form.consent then | 645 elseif form.user_token and form.consent then |
474 -- Second step: consent | 646 -- Second step: consent |
475 local ok, user = verify_user_token(form.user_token); | 647 local ok, user = verify_user_token(form.user_token); |
477 return { | 649 return { |
478 error = user == "token-expired" and "Session expired - try again" or nil; | 650 error = user == "token-expired" and "Session expired - try again" or nil; |
479 }; | 651 }; |
480 end | 652 end |
481 | 653 |
482 local scope = array():append(form):filter(function(field) | 654 local scopes = array():append(form):filter(function(field) |
483 return field.name == "scope" or field.name == "role"; | 655 return field.name == "scope"; |
484 end):pluck("value"):concat(" "); | 656 end):pluck("value"); |
485 | 657 |
486 user.token = form.user_token; | 658 user.token = form.user_token; |
487 return { | 659 return { |
488 user = user; | 660 user = user; |
489 scope = scope; | 661 scopes = scopes; |
490 consent = form.consent == "granted"; | 662 consent = form.consent == "granted"; |
491 }; | 663 }; |
492 end | 664 end |
493 | 665 |
494 return {}; | 666 return {}; |
525 function grant_type_handlers.password(params) | 697 function grant_type_handlers.password(params) |
526 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); | 698 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); |
527 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); | 699 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); |
528 local request_username, request_host, request_resource = jid.prepped_split(request_jid); | 700 local request_username, request_host, request_resource = jid.prepped_split(request_jid); |
529 if params.scope then | 701 if params.scope then |
702 -- TODO shouldn't we support scopes / roles here? | |
530 return oauth_error("invalid_scope", "unknown scope requested"); | 703 return oauth_error("invalid_scope", "unknown scope requested"); |
531 end | 704 end |
532 if not request_host or request_host ~= module.host then | 705 if not request_host or request_host ~= module.host then |
533 return oauth_error("invalid_request", "invalid JID"); | 706 return oauth_error("invalid_request", "invalid JID"); |
534 end | 707 end |
544 response_type_handlers.code = nil; | 717 response_type_handlers.code = nil; |
545 response_type_handlers.token = nil; | 718 response_type_handlers.token = nil; |
546 grant_type_handlers.authorization_code = nil; | 719 grant_type_handlers.authorization_code = nil; |
547 end | 720 end |
548 | 721 |
722 local function render_error(err) | |
723 return render_page(templates.error, { error = err }); | |
724 end | |
725 | |
549 -- OAuth errors should be returned to the client if possible, i.e. by | 726 -- 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 | 727 -- 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 | 728 -- 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 | 729 -- the redirect_uri is missing or invalid. In those cases, we render an |
553 -- error directly to the user-agent. | 730 -- error directly to the user-agent. |
554 local function error_response(request, err) | 731 local function error_response(request, redirect_uri, err) |
555 local q = request.url.query and http.formdecode(request.url.query); | 732 if not redirect_uri or redirect_uri == oob_uri then |
556 local redirect_uri = q and q.redirect_uri; | 733 return render_error(err); |
557 if not redirect_uri or not is_secure_redirect(redirect_uri) then | 734 end |
558 module:log("warn", "Missing or invalid redirect_uri <%s>, rendering error to user-agent", redirect_uri or ""); | 735 local q = strict_formdecode(request.url.query); |
559 return render_page(templates.error, { error = err }); | |
560 end | |
561 local redirect_query = url.parse(redirect_uri); | 736 local redirect_query = url.parse(redirect_uri); |
562 local sep = redirect_query.query and "&" or "?"; | 737 local sep = redirect_query.query and "&" or "?"; |
563 redirect_uri = redirect_uri | 738 redirect_uri = redirect_uri |
564 .. sep .. http.formencode(err.extra.oauth2_response) | 739 .. sep .. http.formencode(err.extra.oauth2_response) |
565 .. "&" .. http.formencode({ state = q.state, iss = get_issuer() }); | 740 .. "&" .. http.formencode({ state = q.state, iss = get_issuer() }); |
566 module:log("warn", "Sending error response to client via redirect to %s", redirect_uri); | 741 module:log("warn", "Sending error response to client via redirect to %s", redirect_uri); |
567 return { | 742 return { |
568 status_code = 303; | 743 status_code = 303; |
569 headers = { | 744 headers = { |
745 cache_control = "no-store"; | |
746 pragma = "no-cache"; | |
570 location = redirect_uri; | 747 location = redirect_uri; |
571 }; | 748 }; |
572 }; | 749 }; |
573 end | 750 end |
574 | 751 |
575 local allowed_grant_type_handlers = module:get_option_set("allowed_oauth2_grant_types", {"authorization_code", "password", "refresh_token"}) | 752 local allowed_grant_type_handlers = module:get_option_set("allowed_oauth2_grant_types", { |
753 "authorization_code"; | |
754 "password"; -- TODO Disable. The resource owner password credentials grant [RFC6749] MUST NOT be used. | |
755 "refresh_token"; | |
756 device_uri; | |
757 }) | |
758 if allowed_grant_type_handlers:contains("device_code") then | |
759 -- expand short form because that URI is long | |
760 module:log("debug", "Expanding %q to %q in '%s'", "device_code", device_uri, "allowed_oauth2_grant_types"); | |
761 allowed_grant_type_handlers:remove("device_code"); | |
762 allowed_grant_type_handlers:add(device_uri); | |
763 end | |
576 for handler_type in pairs(grant_type_handlers) do | 764 for handler_type in pairs(grant_type_handlers) do |
577 if not allowed_grant_type_handlers:contains(handler_type) then | 765 if not allowed_grant_type_handlers:contains(handler_type) then |
578 module:log("debug", "Grant type %q disabled", handler_type); | 766 module:log("debug", "Grant type %q disabled", handler_type); |
579 grant_type_handlers[handler_type] = nil; | 767 grant_type_handlers[handler_type] = nil; |
580 else | 768 else |
605 | 793 |
606 function handle_token_grant(event) | 794 function handle_token_grant(event) |
607 local credentials = get_request_credentials(event.request); | 795 local credentials = get_request_credentials(event.request); |
608 | 796 |
609 event.response.headers.content_type = "application/json"; | 797 event.response.headers.content_type = "application/json"; |
610 local params = http.formdecode(event.request.body); | 798 event.response.headers.cache_control = "no-store"; |
799 event.response.headers.pragma = "no-cache"; | |
800 local params = strict_formdecode(event.request.body); | |
611 if not params then | 801 if not params then |
612 return error_response(event.request, oauth_error("invalid_request")); | 802 return oauth_error("invalid_request", "Could not parse request body as 'application/x-www-form-urlencoded'"); |
613 end | 803 end |
614 | 804 |
615 if credentials and credentials.type == "basic" then | 805 if credentials and credentials.type == "basic" then |
616 -- client_secret_basic converted internally to client_secret_post | 806 -- client_secret_basic converted internally to client_secret_post |
617 params.client_id = http.urldecode(credentials.username); | 807 params.client_id = http.urldecode(credentials.username); |
619 end | 809 end |
620 | 810 |
621 local grant_type = params.grant_type | 811 local grant_type = params.grant_type |
622 local grant_handler = grant_type_handlers[grant_type]; | 812 local grant_handler = grant_type_handlers[grant_type]; |
623 if not grant_handler then | 813 if not grant_handler then |
624 return error_response(event.request, oauth_error("unsupported_grant_type")); | 814 return oauth_error("invalid_request", "No such grant type."); |
625 end | 815 end |
626 return grant_handler(params); | 816 return grant_handler(params); |
627 end | 817 end |
628 | 818 |
629 local function handle_authorization_request(event) | 819 local function handle_authorization_request(event) |
630 local request = event.request; | 820 local request = event.request; |
631 | 821 |
822 -- Directly returning errors to the user before we have a validated client object | |
632 if not request.url.query then | 823 if not request.url.query then |
633 return error_response(request, oauth_error("invalid_request")); | 824 return render_error(oauth_error("invalid_request", "Missing query parameters")); |
634 end | 825 end |
635 local params = http.formdecode(request.url.query); | 826 local params = strict_formdecode(request.url.query); |
636 if not params then | 827 if not params then |
637 return error_response(request, oauth_error("invalid_request")); | 828 return render_error(oauth_error("invalid_request", "Invalid query parameters")); |
638 end | 829 end |
639 | 830 |
640 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end | 831 if not params.client_id then |
641 | 832 return render_error(oauth_error("invalid_request", "Missing 'client_id' parameter")); |
642 local ok, client = jwt_verify(params.client_id); | 833 end |
643 | 834 |
644 if not ok then | 835 local client = check_client(params.client_id); |
645 return oauth_error("invalid_client", "incorrect credentials"); | 836 |
646 end | 837 if not client then |
838 return render_error(oauth_error("invalid_request", "Invalid 'client_id' parameter")); | |
839 end | |
840 | |
841 local redirect_uri = get_redirect_uri(client, params.redirect_uri); | |
842 if not redirect_uri then | |
843 return render_error(oauth_error("invalid_request", "Invalid 'redirect_uri' parameter")); | |
844 end | |
845 -- From this point we know that redirect_uri is safe to use | |
647 | 846 |
648 local client_response_types = set.new(array(client.response_types or { "code" })); | 847 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); | 848 client_response_types = set.intersection(client_response_types, allowed_response_type_handlers); |
650 if not client_response_types:contains(params.response_type) then | 849 if not client_response_types:contains(params.response_type) then |
651 return oauth_error("invalid_client", "response_type not allowed"); | 850 return error_response(request, redirect_uri, oauth_error("invalid_client", "'response_type' not allowed")); |
851 end | |
852 | |
853 local requested_scopes = parse_scopes(params.scope or ""); | |
854 if client.scope then | |
855 local client_scopes = set.new(parse_scopes(client.scope)); | |
856 requested_scopes:filter(function(scope) | |
857 return client_scopes:contains(scope); | |
858 end); | |
859 end | |
860 | |
861 -- The 'prompt' parameter from OpenID Core | |
862 local prompt = set.new(parse_scopes(params.prompt or "select_account login consent")); | |
863 if prompt:contains("none") then | |
864 -- Client wants no interaction, only confirmation of prior login and | |
865 -- consent, but this is not implemented. | |
866 return error_response(request, redirect_uri, oauth_error("interaction_required")); | |
867 elseif not prompt:contains("select_account") and not params.login_hint then | |
868 -- TODO If the login page is split into account selection followed by login | |
869 -- (e.g. password), and then the account selection could be skipped iff the | |
870 -- 'login_hint' parameter is present. | |
871 return error_response(request, redirect_uri, oauth_error("account_selection_required")); | |
872 elseif not prompt:contains("login") then | |
873 -- Currently no cookies or such are used, so login is required every time. | |
874 return error_response(request, redirect_uri, oauth_error("login_required")); | |
875 elseif not prompt:contains("consent") then | |
876 -- Are there any circumstances when consent would be implied or assumed? | |
877 return error_response(request, redirect_uri, oauth_error("consent_required")); | |
652 end | 878 end |
653 | 879 |
654 local auth_state = get_auth_state(request); | 880 local auth_state = get_auth_state(request); |
655 if not auth_state.user then | 881 if not auth_state.user then |
656 -- Render login page | 882 -- Render login page |
657 return render_page(templates.login, { state = auth_state, client = client }); | 883 local extra = {}; |
884 if params.login_hint then | |
885 extra.username_hint = (jid.prepped_split(params.login_hint)); | |
886 end | |
887 return render_page(templates.login, { state = auth_state; client = client; extra = extra }); | |
658 elseif auth_state.consent == nil then | 888 elseif auth_state.consent == nil then |
659 -- Render consent page | 889 -- Render consent page |
660 local scopes, requested_roles = split_scopes(parse_scopes(params.scope or "")); | 890 local scopes, roles = split_scopes(requested_scopes); |
661 local default_role = select_role(auth_state.user.username, requested_roles); | 891 roles = user_assumable_roles(auth_state.user.username, roles); |
662 local roles = array(it.values(usermanager.get_all_roles(module.host))):filter(function(role) | 892 return render_page(templates.consent, { state = auth_state; client = client; scopes = scopes+roles }, true); |
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 | 893 elseif not auth_state.consent then |
675 -- Notify client of rejection | 894 -- Notify client of rejection |
676 return error_response(request, oauth_error("access_denied")); | 895 if redirect_uri == device_uri then |
896 local is_device, device_state = verify_device_token(params.state); | |
897 if is_device then | |
898 local device_code = b64url(hashes.hmac_sha256(verification_key, device_state.user_code)); | |
899 local code = codes:get("device_code:" .. params.client_id .. "#" .. device_code); | |
900 code.error = oauth_error("access_denied"); | |
901 code.expires = os.time() + 60; | |
902 codes:set("device_code:" .. params.client_id .. "#" .. device_code, code); | |
903 end | |
904 end | |
905 return error_response(request, redirect_uri, oauth_error("access_denied")); | |
677 end | 906 end |
678 -- else auth_state.consent == true | 907 -- else auth_state.consent == true |
679 | 908 |
680 params.scope = auth_state.scope; | 909 local granted_scopes = auth_state.scopes |
910 if client.scope then | |
911 local client_scopes = set.new(parse_scopes(client.scope)); | |
912 granted_scopes:filter(function(scope) | |
913 return client_scopes:contains(scope); | |
914 end); | |
915 end | |
916 | |
917 params.scope = granted_scopes:concat(" "); | |
681 | 918 |
682 local user_jid = jid.join(auth_state.user.username, module.host); | 919 local user_jid = jid.join(auth_state.user.username, module.host); |
683 local client_secret = make_client_secret(params.client_id); | 920 local client_secret = make_client_secret(params.client_id); |
684 local id_token_signer = jwt.new_signer("HS256", client_secret); | 921 local id_token_signer = jwt.new_signer("HS256", client_secret); |
685 local id_token = id_token_signer({ | 922 local id_token = id_token_signer({ |
686 iss = get_issuer(); | 923 iss = get_issuer(); |
687 sub = url.build({ scheme = "xmpp"; path = user_jid }); | 924 sub = url.build({ scheme = "xmpp"; path = user_jid }); |
688 aud = params.client_id; | 925 aud = params.client_id; |
926 auth_time = auth_state.user.auth_time; | |
689 nonce = params.nonce; | 927 nonce = params.nonce; |
690 }); | 928 }); |
691 local response_type = params.response_type; | 929 local response_type = params.response_type; |
692 local response_handler = response_type_handlers[response_type]; | 930 local response_handler = response_type_handlers[response_type]; |
693 if not response_handler then | 931 if not response_handler then |
694 return error_response(request, oauth_error("unsupported_response_type")); | 932 return error_response(request, redirect_uri, oauth_error("unsupported_response_type")); |
695 end | 933 end |
696 return response_handler(client, params, user_jid, id_token); | 934 local ret = response_handler(client, params, user_jid, id_token); |
697 end | 935 if errors.is_err(ret) then |
936 return error_response(request, redirect_uri, ret); | |
937 end | |
938 return ret; | |
939 end | |
940 | |
941 local function handle_device_authorization_request(event) | |
942 local request = event.request; | |
943 | |
944 local credentials = get_request_credentials(request); | |
945 | |
946 local params = strict_formdecode(request.body); | |
947 if not params then | |
948 return render_error(oauth_error("invalid_request", "Invalid query parameters")); | |
949 end | |
950 | |
951 if credentials and credentials.type == "basic" then | |
952 -- client_secret_basic converted internally to client_secret_post | |
953 params.client_id = http.urldecode(credentials.username); | |
954 local client_secret = http.urldecode(credentials.password); | |
955 | |
956 if not verify_client_secret(params.client_id, client_secret) then | |
957 module:log("debug", "client_secret mismatch"); | |
958 return oauth_error("invalid_client", "incorrect credentials"); | |
959 end | |
960 else | |
961 return 401; | |
962 end | |
963 | |
964 local client = check_client(params.client_id); | |
965 | |
966 if not client then | |
967 return render_error(oauth_error("invalid_request", "Invalid 'client_id' parameter")); | |
968 end | |
969 | |
970 if not set.new(client.grant_types):contains(device_uri) then | |
971 return render_error(oauth_error("invalid_client", "Client not registered for device authorization grant")); | |
972 end | |
973 | |
974 local requested_scopes = parse_scopes(params.scope or ""); | |
975 if client.scope then | |
976 local client_scopes = set.new(parse_scopes(client.scope)); | |
977 requested_scopes:filter(function(scope) | |
978 return client_scopes:contains(scope); | |
979 end); | |
980 end | |
981 | |
982 -- TODO better code generator, this one should be easy to type from a | |
983 -- screen onto a phone | |
984 local user_code = (id.tiny() .. "-" .. id.tiny()):upper(); | |
985 local collisions = 0; | |
986 while codes:get("authorization_code:" .. device_uri .. "#" .. user_code) do | |
987 collisions = collisions + 1; | |
988 if collisions > 10 then | |
989 return oauth_error("temporarily_unavailable"); | |
990 end | |
991 user_code = (id.tiny() .. "-" .. id.tiny()):upper(); | |
992 end | |
993 -- device code should be derivable after consent but not guessable by the user | |
994 local device_code = b64url(hashes.hmac_sha256(verification_key, user_code)); | |
995 local verification_uri = module:http_url() .. "/device"; | |
996 local verification_uri_complete = verification_uri .. "?" .. http.formencode({ user_code = user_code }); | |
997 | |
998 local expires = os.time() + 600; | |
999 local dc_ok = codes:set("device_code:" .. params.client_id .. "#" .. device_code, { expires = expires }); | |
1000 local uc_ok = codes:set("user_code:" .. user_code, | |
1001 { user_code = user_code; expires = expires; client_id = params.client_id; | |
1002 scope = requested_scopes:concat(" ") }); | |
1003 if not dc_ok or not uc_ok then | |
1004 return oauth_error("temporarily_unavailable"); | |
1005 end | |
1006 | |
1007 return { | |
1008 headers = { content_type = "application/json"; cache_control = "no-store"; pragma = "no-cache" }; | |
1009 body = json.encode { | |
1010 device_code = device_code; | |
1011 user_code = user_code; | |
1012 verification_uri = verification_uri; | |
1013 verification_uri_complete = verification_uri_complete; | |
1014 expires_in = 600; | |
1015 interval = 5; | |
1016 }; | |
1017 } | |
1018 end | |
1019 | |
1020 local function handle_device_verification_request(event) | |
1021 local request = event.request; | |
1022 local params = strict_formdecode(request.url.query); | |
1023 if not params or not params.user_code then | |
1024 return render_page(templates.device, { client = false }); | |
1025 end | |
1026 | |
1027 local device_info = codes:get("user_code:" .. params.user_code); | |
1028 if not device_info or code_expired(device_info) or not codes:set("user_code:" .. params.user_code, nil) then | |
1029 return render_page(templates.device, { | |
1030 client = false; | |
1031 error = oauth_error("expired_token", "Incorrect or expired code"); | |
1032 }); | |
1033 end | |
1034 | |
1035 return { | |
1036 status_code = 303; | |
1037 headers = { | |
1038 location = module:http_url() .. "/authorize" .. "?" .. http.formencode({ | |
1039 client_id = device_info.client_id; | |
1040 redirect_uri = device_uri; | |
1041 response_type = "code"; | |
1042 scope = device_info.scope; | |
1043 state = new_device_token({ user_code = params.user_code }); | |
1044 }); | |
1045 }; | |
1046 } | |
1047 end | |
1048 | |
1049 local strict_auth_revoke = module:get_option_boolean("oauth2_require_auth_revoke", false); | |
698 | 1050 |
699 local function handle_revocation_request(event) | 1051 local function handle_revocation_request(event) |
700 local request, response = event.request, event.response; | 1052 local request, response = event.request, event.response; |
1053 response.headers.cache_control = "no-store"; | |
1054 response.headers.pragma = "no-cache"; | |
701 if request.headers.authorization then | 1055 if request.headers.authorization then |
702 local credentials = get_request_credentials(request); | 1056 local credentials = get_request_credentials(request); |
703 if not credentials or credentials.type ~= "basic" then | 1057 if not credentials or credentials.type ~= "basic" then |
704 response.headers.www_authenticate = string.format("Basic realm=%q", module.host.."/"..module.name); | 1058 response.headers.www_authenticate = string.format("Basic realm=%q", module.host.."/"..module.name); |
705 return 401; | 1059 return 401; |
706 end | 1060 end |
707 -- OAuth "client" credentials | 1061 -- OAuth "client" credentials |
708 if not verify_client_secret(credentials.username, credentials.password) then | 1062 if not verify_client_secret(credentials.username, credentials.password) then |
709 return 401; | 1063 return 401; |
710 end | 1064 end |
711 end | 1065 -- TODO check that it's their token I guess? |
712 | 1066 elseif strict_auth_revoke then |
713 local form_data = http.formdecode(event.request.body or ""); | 1067 -- Why require auth to revoke a leaked token? |
1068 response.headers.www_authenticate = string.format("Basic realm=%q", module.host.."/"..module.name); | |
1069 return 401; | |
1070 end | |
1071 | |
1072 local form_data = strict_formdecode(event.request.body); | |
714 if not form_data or not form_data.token then | 1073 if not form_data or not form_data.token then |
715 response.headers.accept = "application/x-www-form-urlencoded"; | 1074 response.headers.accept = "application/x-www-form-urlencoded"; |
716 return 415; | 1075 return 415; |
717 end | 1076 end |
718 local ok, err = tokens.revoke_token(form_data.token); | 1077 local ok, err = tokens.revoke_token(form_data.token); |
722 end | 1081 end |
723 return 200; | 1082 return 200; |
724 end | 1083 end |
725 | 1084 |
726 local registration_schema = { | 1085 local registration_schema = { |
1086 title = "OAuth 2.0 Dynamic Client Registration Protocol"; | |
727 type = "object"; | 1087 type = "object"; |
728 required = { | 1088 required = { |
729 -- These are shown to users in the template | 1089 -- These are shown to users in the template |
730 "client_name"; | 1090 "client_name"; |
731 "client_uri"; | 1091 "client_uri"; |
732 -- We need at least one redirect URI for things to work | 1092 -- We need at least one redirect URI for things to work |
733 "redirect_uris"; | 1093 "redirect_uris"; |
734 }; | 1094 }; |
735 properties = { | 1095 properties = { |
736 redirect_uris = { type = "array"; minLength = 1; items = { type = "string"; format = "uri" } }; | 1096 redirect_uris = { |
1097 title = "List of Redirect URIs"; | |
1098 type = "array"; | |
1099 minItems = 1; | |
1100 uniqueItems = true; | |
1101 items = { title = "Redirect URI"; type = "string"; format = "uri" }; | |
1102 }; | |
737 token_endpoint_auth_method = { | 1103 token_endpoint_auth_method = { |
1104 title = "Token Endpoint Authentication Method"; | |
738 type = "string"; | 1105 type = "string"; |
739 enum = { "none"; "client_secret_post"; "client_secret_basic" }; | 1106 enum = { "none"; "client_secret_post"; "client_secret_basic" }; |
740 default = "client_secret_basic"; | 1107 default = "client_secret_basic"; |
741 }; | 1108 }; |
742 grant_types = { | 1109 grant_types = { |
1110 title = "Grant Types"; | |
743 type = "array"; | 1111 type = "array"; |
1112 minItems = 1; | |
1113 uniqueItems = true; | |
744 items = { | 1114 items = { |
745 type = "string"; | 1115 type = "string"; |
746 enum = { | 1116 enum = { |
747 "authorization_code"; | 1117 "authorization_code"; |
748 "implicit"; | 1118 "implicit"; |
749 "password"; | 1119 "password"; |
750 "client_credentials"; | 1120 "client_credentials"; |
751 "refresh_token"; | 1121 "refresh_token"; |
752 "urn:ietf:params:oauth:grant-type:jwt-bearer"; | 1122 "urn:ietf:params:oauth:grant-type:jwt-bearer"; |
753 "urn:ietf:params:oauth:grant-type:saml2-bearer"; | 1123 "urn:ietf:params:oauth:grant-type:saml2-bearer"; |
1124 device_uri; | |
754 }; | 1125 }; |
755 }; | 1126 }; |
756 default = { "authorization_code" }; | 1127 default = { "authorization_code" }; |
757 }; | 1128 }; |
758 application_type = { type = "string"; enum = { "native"; "web" }; default = "web" }; | 1129 application_type = { |
759 response_types = { type = "array"; items = { type = "string"; enum = { "code"; "token" } }; default = { "code" } }; | 1130 title = "Application Type"; |
760 client_name = { type = "string" }; | 1131 description = "Determines which kinds of redirect URIs the client may register. \z |
761 client_uri = { type = "string"; format = "uri"; luaPattern = "^https:" }; | 1132 The value 'web' limits the client to https:// URLs with the same hostname as in 'client_uri' \z |
762 logo_uri = { type = "string"; format = "uri"; luaPattern = "^https:" }; | 1133 while the value 'native' allows either loopback http:// URLs or application specific URIs."; |
763 scope = { type = "string" }; | 1134 type = "string"; |
764 contacts = { type = "array"; items = { type = "string"; format = "email" } }; | 1135 enum = { "native"; "web" }; |
765 tos_uri = { type = "string"; format = "uri"; luaPattern = "^https:" }; | 1136 default = "web"; |
766 policy_uri = { type = "string"; format = "uri"; luaPattern = "^https:" }; | 1137 }; |
767 jwks_uri = { type = "string"; format = "uri"; luaPattern = "^https:" }; | 1138 response_types = { |
768 jwks = { type = "object"; description = "JSON Web Key Set, RFC 7517" }; | 1139 title = "Response Types"; |
769 software_id = { type = "string"; format = "uuid" }; | 1140 type = "array"; |
770 software_version = { type = "string" }; | 1141 minItems = 1; |
771 }; | 1142 uniqueItems = true; |
772 luaPatternProperties = { | 1143 items = { type = "string"; enum = { "code"; "token" } }; |
773 -- Localized versions of descriptive properties and URIs | 1144 default = { "code" }; |
774 ["^client_name#"] = { description = "Localized version of 'client_name'"; type = "string" }; | 1145 }; |
775 ["^[a-z_]+_uri#"] = { type = "string"; format = "uri"; luaPattern = "^https:" }; | 1146 client_name = { |
1147 title = "Client Name"; | |
1148 description = "Human-readable name of the client, presented to the user in the consent dialog."; | |
1149 type = "string"; | |
1150 }; | |
1151 client_uri = { | |
1152 title = "Client URL"; | |
1153 description = "Should be an link to a page with information about the client."; | |
1154 type = "string"; | |
1155 format = "uri"; | |
1156 pattern = "^https:"; | |
1157 }; | |
1158 logo_uri = { | |
1159 title = "Logo URL"; | |
1160 description = "URL to the clients logotype (not currently used)."; | |
1161 type = "string"; | |
1162 format = "uri"; | |
1163 pattern = "^https:"; | |
1164 }; | |
1165 scope = { | |
1166 title = "Scopes"; | |
1167 description = "Space-separated list of scopes the client promises to restrict itself to."; | |
1168 type = "string"; | |
1169 }; | |
1170 contacts = { | |
1171 title = "Contact Addresses"; | |
1172 description = "Addresses, typically email or URLs where the client developers can be contacted."; | |
1173 type = "array"; | |
1174 minItems = 1; | |
1175 items = { type = "string"; format = "email" }; | |
1176 }; | |
1177 tos_uri = { | |
1178 title = "Terms of Service URL"; | |
1179 description = "Link to Terms of Service for the client, presented to the user in the consent dialog. \z | |
1180 MUST be a https:// URL with hostname matching that of 'client_uri'."; | |
1181 type = "string"; | |
1182 format = "uri"; | |
1183 pattern = "^https:"; | |
1184 }; | |
1185 policy_uri = { | |
1186 title = "Privacy Policy URL"; | |
1187 description = "Link to a Privacy Policy for the client. MUST be a https:// URL with hostname matching that of 'client_uri'."; | |
1188 type = "string"; | |
1189 format = "uri"; | |
1190 pattern = "^https:"; | |
1191 }; | |
1192 software_id = { | |
1193 title = "Software ID"; | |
1194 description = "Unique identifier for the client software, common for all instances. Typically an UUID."; | |
1195 type = "string"; | |
1196 format = "uuid"; | |
1197 }; | |
1198 software_version = { | |
1199 title = "Software Version"; | |
1200 description = "Version of the client software being registered. \z | |
1201 E.g. to allow revoking all related tokens in the event of a security incident."; | |
1202 type = "string"; | |
1203 example = "2.3.1"; | |
1204 }; | |
776 }; | 1205 }; |
777 } | 1206 } |
778 | 1207 |
1208 -- Limit per-locale fields to allowed locales, partly to keep size of client_id | |
1209 -- down, partly because we don't yet use them for anything. | |
1210 -- Only relevant for user-visible strings and URIs. | |
1211 if allowed_locales[1] then | |
1212 local props = registration_schema.properties; | |
1213 for _, locale in ipairs(allowed_locales) do | |
1214 props["client_name#" .. locale] = props["client_name"]; | |
1215 props["client_uri#" .. locale] = props["client_uri"]; | |
1216 props["logo_uri#" .. locale] = props["logo_uri"]; | |
1217 props["tos_uri#" .. locale] = props["tos_uri"]; | |
1218 props["policy_uri#" .. locale] = props["policy_uri"]; | |
1219 end | |
1220 end | |
1221 | |
779 local function redirect_uri_allowed(redirect_uri, client_uri, app_type) | 1222 local function redirect_uri_allowed(redirect_uri, client_uri, app_type) |
780 local uri = url.parse(redirect_uri); | 1223 local uri = url.parse(redirect_uri); |
1224 if not uri.scheme then | |
1225 return false; -- no relative URLs | |
1226 end | |
781 if app_type == "native" then | 1227 if app_type == "native" then |
782 return uri.scheme == "http" and loopbacks:contains(uri.host) or uri.scheme ~= "https"; | 1228 return uri.scheme == "http" and loopbacks:contains(uri.host) or redirect_uri == oob_uri or uri.scheme:find(".", 1, true) ~= nil; |
783 elseif app_type == "web" then | 1229 elseif app_type == "web" then |
784 return uri.scheme == "https" and uri.host == client_uri.host; | 1230 return uri.scheme == "https" and uri.host == client_uri.host; |
785 end | 1231 end |
786 end | 1232 end |
787 | 1233 |
788 function create_client(client_metadata) | 1234 function create_client(client_metadata) |
789 if not schema.validate(registration_schema, client_metadata) then | 1235 if not schema.validate(registration_schema, client_metadata) then |
790 return nil, oauth_error("invalid_request", "Failed schema validation."); | 1236 return nil, oauth_error("invalid_request", "Failed schema validation."); |
1237 end | |
1238 | |
1239 local client_uri = url.parse(client_metadata.client_uri); | |
1240 if not client_uri or client_uri.scheme ~= "https" or loopbacks:contains(client_uri.host) then | |
1241 return nil, oauth_error("invalid_client_metadata", "Missing, invalid or insecure client_uri"); | |
1242 end | |
1243 | |
1244 if not client_metadata.application_type and redirect_uri_allowed(client_metadata.redirect_uris[1], client_uri, "native") then | |
1245 client_metadata.application_type = "native"; | |
1246 -- else defaults to "web" | |
791 end | 1247 end |
792 | 1248 |
793 -- Fill in default values | 1249 -- Fill in default values |
794 for propname, propspec in pairs(registration_schema.properties) do | 1250 for propname, propspec in pairs(registration_schema.properties) do |
795 if client_metadata[propname] == nil and type(propspec) == "table" and propspec.default ~= nil then | 1251 if client_metadata[propname] == nil and type(propspec) == "table" and propspec.default ~= nil then |
796 client_metadata[propname] = propspec.default; | 1252 client_metadata[propname] = propspec.default; |
797 end | 1253 end |
798 end | 1254 end |
799 | 1255 |
800 local client_uri = url.parse(client_metadata.client_uri); | 1256 -- MUST ignore any metadata that it does not understand |
801 if not client_uri or client_uri.scheme ~= "https" or loopbacks:contains(client_uri.host) then | 1257 for propname in pairs(client_metadata) do |
802 return nil, oauth_error("invalid_client_metadata", "Missing, invalid or insecure client_uri"); | 1258 if not registration_schema.properties[propname] then |
1259 client_metadata[propname] = nil; | |
1260 end | |
803 end | 1261 end |
804 | 1262 |
805 for _, redirect_uri in ipairs(client_metadata.redirect_uris) do | 1263 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 | 1264 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."); | 1265 return nil, oauth_error("invalid_redirect_uri", "Invalid, insecure or inappropriate redirect URI."); |
814 return nil, oauth_error("invalid_client_metadata", "Invalid, insecure or inappropriate informative URI"); | 1272 return nil, oauth_error("invalid_client_metadata", "Invalid, insecure or inappropriate informative URI"); |
815 end | 1273 end |
816 end | 1274 end |
817 end | 1275 end |
818 | 1276 |
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); | 1277 local grant_types = set.new(client_metadata.grant_types); |
833 local response_types = set.new(client_metadata.response_types); | 1278 local response_types = set.new(client_metadata.response_types); |
834 | 1279 |
835 if grant_types:contains("authorization_code") and not response_types:contains("code") then | 1280 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'"); | 1281 return nil, oauth_error("invalid_client_metadata", "Inconsistency between 'grant_types' and 'response_types'"); |
842 return nil, oauth_error("invalid_client_metadata", "No allowed 'grant_types' specified"); | 1287 return nil, oauth_error("invalid_client_metadata", "No allowed 'grant_types' specified"); |
843 elseif set.intersection(response_types, allowed_response_type_handlers):empty() then | 1288 elseif set.intersection(response_types, allowed_response_type_handlers):empty() then |
844 return nil, oauth_error("invalid_client_metadata", "No allowed 'response_types' specified"); | 1289 return nil, oauth_error("invalid_client_metadata", "No allowed 'response_types' specified"); |
845 end | 1290 end |
846 | 1291 |
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? | 1292 -- Do we want to keep everything? |
852 local client_id = jwt_sign(client_metadata); | 1293 local client_id = sign_client(client_metadata); |
853 | 1294 |
854 client_metadata.client_id = client_id; | 1295 client_metadata.client_id = client_id; |
855 client_metadata.client_id_issued_at = os.time(); | 1296 client_metadata.client_id_issued_at = os.time(); |
856 | 1297 |
857 if client_metadata.token_endpoint_auth_method ~= "none" then | 1298 if client_metadata.token_endpoint_auth_method ~= "none" then |
858 local client_secret = make_client_secret(client_id); | 1299 -- Ensure that each client_id JWT with a client_secret is unique. |
1300 -- A short ID along with the issued at timestamp should be sufficient to | |
1301 -- rule out brute force attacks. | |
1302 -- Not needed for public clients without a secret, but those are expected | |
1303 -- to be uncommon since they can only do the insecure implicit flow. | |
1304 client_metadata.nonce = id.short(); | |
1305 | |
1306 local client_secret = make_client_secret(client_id, client_metadata); | |
859 client_metadata.client_secret = client_secret; | 1307 client_metadata.client_secret = client_secret; |
860 client_metadata.client_secret_expires_at = 0; | 1308 client_metadata.client_secret_expires_at = 0; |
861 | 1309 |
862 if not registration_options.accept_expired then | 1310 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); | 1311 client_metadata.client_secret_expires_at = client_metadata.client_id_issued_at + (registration_options.default_ttl or 3600); |
877 local response, err = create_client(client_metadata); | 1325 local response, err = create_client(client_metadata); |
878 if err then return err end | 1326 if err then return err end |
879 | 1327 |
880 return { | 1328 return { |
881 status_code = 201; | 1329 status_code = 201; |
882 headers = { content_type = "application/json" }; | 1330 headers = { |
1331 cache_control = "no-store"; | |
1332 pragma = "no-cache"; | |
1333 content_type = "application/json"; | |
1334 }; | |
883 body = json.encode(response); | 1335 body = json.encode(response); |
884 }; | 1336 }; |
885 end | 1337 end |
886 | 1338 |
887 if not registration_key then | 1339 if not registration_key then |
888 module:log("info", "No 'oauth2_registration_key', dynamic client registration disabled") | 1340 module:log("info", "No 'oauth2_registration_key', dynamic client registration disabled") |
889 handle_authorization_request = nil | 1341 handle_authorization_request = nil |
890 handle_register_request = nil | 1342 handle_register_request = nil |
1343 handle_device_authorization_request = nil | |
1344 handle_device_verification_request = nil | |
891 end | 1345 end |
892 | 1346 |
893 local function handle_userinfo_request(event) | 1347 local function handle_userinfo_request(event) |
894 local request = event.request; | 1348 local request = event.request; |
895 local credentials = get_request_credentials(request); | 1349 local credentials = get_request_credentials(request); |
939 }; | 1393 }; |
940 end | 1394 end |
941 | 1395 |
942 module:depends("http"); | 1396 module:depends("http"); |
943 module:provides("http", { | 1397 module:provides("http", { |
1398 cors = { enabled = true; credentials = true }; | |
944 route = { | 1399 route = { |
945 -- OAuth 2.0 in 5 simple steps! | 1400 -- OAuth 2.0 in 5 simple steps! |
946 -- This is the normal 'authorization_code' flow. | 1401 -- This is the normal 'authorization_code' flow. |
947 | 1402 |
948 -- Step 1. Create OAuth client | 1403 -- Step 1. Create OAuth client |
949 ["POST /register"] = handle_register_request; | 1404 ["POST /register"] = handle_register_request; |
950 | 1405 |
1406 -- Device flow | |
1407 ["POST /device"] = handle_device_authorization_request; | |
1408 ["GET /device"] = handle_device_verification_request; | |
1409 | |
951 -- Step 2. User-facing login and consent view | 1410 -- Step 2. User-facing login and consent view |
952 ["GET /authorize"] = handle_authorization_request; | 1411 ["GET /authorize"] = handle_authorization_request; |
953 ["POST /authorize"] = handle_authorization_request; | 1412 ["POST /authorize"] = handle_authorization_request; |
1413 ["OPTIONS /authorize"] = { status_code = 403; body = "" }; | |
954 | 1414 |
955 -- Step 3. User is redirected to the 'redirect_uri' along with an | 1415 -- Step 3. User is redirected to the 'redirect_uri' along with an |
956 -- authorization code. In the insecure 'implicit' flow, the access token | 1416 -- authorization code. In the insecure 'implicit' flow, the access token |
957 -- is delivered here. | 1417 -- is delivered here. |
958 | 1418 |
970 -- Optional static content for templates | 1430 -- Optional static content for templates |
971 ["GET /style.css"] = templates.css and { | 1431 ["GET /style.css"] = templates.css and { |
972 headers = { | 1432 headers = { |
973 ["Content-Type"] = "text/css"; | 1433 ["Content-Type"] = "text/css"; |
974 }; | 1434 }; |
975 body = _render_html(templates.css, module:get_option("oauth2_template_style")); | 1435 body = templates.css; |
976 } or nil; | 1436 } or nil; |
977 ["GET /script.js"] = templates.js and { | 1437 ["GET /script.js"] = templates.js and { |
978 headers = { | 1438 headers = { |
979 ["Content-Type"] = "text/javascript"; | 1439 ["Content-Type"] = "text/javascript"; |
980 }; | 1440 }; |
1000 return json.encode(oauth2_response); | 1460 return json.encode(oauth2_response); |
1001 end, 5); | 1461 end, 5); |
1002 | 1462 |
1003 -- OIDC Discovery | 1463 -- OIDC Discovery |
1004 | 1464 |
1465 function get_authorization_server_metadata() | |
1466 if authorization_server_metadata then | |
1467 return authorization_server_metadata; | |
1468 end | |
1469 authorization_server_metadata = { | |
1470 -- RFC 8414: OAuth 2.0 Authorization Server Metadata | |
1471 issuer = get_issuer(); | |
1472 authorization_endpoint = handle_authorization_request and module:http_url() .. "/authorize" or nil; | |
1473 token_endpoint = handle_token_grant and module:http_url() .. "/token" or nil; | |
1474 registration_endpoint = handle_register_request and module:http_url() .. "/register" or nil; | |
1475 scopes_supported = usermanager.get_all_roles | |
1476 and array(it.keys(usermanager.get_all_roles(module.host))):push("xmpp"):append(array(openid_claims:items())); | |
1477 response_types_supported = array(it.keys(response_type_handlers)); | |
1478 token_endpoint_auth_methods_supported = array({ "client_secret_post"; "client_secret_basic" }); | |
1479 op_policy_uri = module:get_option_string("oauth2_policy_url", nil); | |
1480 op_tos_uri = module:get_option_string("oauth2_terms_url", nil); | |
1481 revocation_endpoint = handle_revocation_request and module:http_url() .. "/revoke" or nil; | |
1482 revocation_endpoint_auth_methods_supported = array({ "client_secret_basic" }); | |
1483 device_authorization_endpoint = handle_device_authorization_request and module:http_url() .. "/device"; | |
1484 code_challenge_methods_supported = array(it.keys(verifier_transforms)); | |
1485 grant_types_supported = array(it.keys(grant_type_handlers)); | |
1486 response_modes_supported = array(it.keys(response_type_handlers)):map(tmap { token = "fragment"; code = "query" }); | |
1487 authorization_response_iss_parameter_supported = true; | |
1488 service_documentation = module:get_option_string("oauth2_service_documentation", "https://modules.prosody.im/mod_http_oauth2.html"); | |
1489 ui_locales_supported = allowed_locales[1] and allowed_locales; | |
1490 | |
1491 -- OpenID | |
1492 userinfo_endpoint = handle_register_request and module:http_url() .. "/userinfo" or nil; | |
1493 jwks_uri = nil; -- REQUIRED in OpenID Discovery but not in OAuth 2.0 Metadata | |
1494 id_token_signing_alg_values_supported = { "HS256" }; -- The algorithm RS256 MUST be included, but we use HS256 and client_secret as shared key. | |
1495 } | |
1496 return authorization_server_metadata; | |
1497 end | |
1498 | |
1005 module:provides("http", { | 1499 module:provides("http", { |
1006 name = "oauth2-discovery"; | 1500 name = "oauth2-discovery"; |
1007 default_path = "/.well-known/oauth-authorization-server"; | 1501 default_path = "/.well-known/oauth-authorization-server"; |
1502 cors = { enabled = true }; | |
1008 route = { | 1503 route = { |
1009 ["GET"] = { | 1504 ["GET"] = function() |
1010 headers = { content_type = "application/json" }; | 1505 return { |
1011 body = json.encode { | 1506 headers = { content_type = "application/json" }; |
1012 -- RFC 8414: OAuth 2.0 Authorization Server Metadata | 1507 body = json.encode(get_authorization_server_metadata()); |
1013 issuer = get_issuer(); | 1508 } |
1014 authorization_endpoint = handle_authorization_request and module:http_url() .. "/authorize" or nil; | 1509 end |
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 }; | 1510 }; |
1037 }); | 1511 }); |
1038 | 1512 |
1039 module:shared("tokenauth/oauthbearer_config").oidc_discovery_url = module:http_url("oauth2-discovery", "/.well-known/oauth-authorization-server"); | 1513 module:shared("tokenauth/oauthbearer_config").oidc_discovery_url = module:http_url("oauth2-discovery", "/.well-known/oauth-authorization-server"); |