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"); |
