Software / code / prosody-modules
Comparison
mod_http_oauth2/mod_http_oauth2.lua @ 5673:0eb2d5ea2428
merge
| author | Stephen Paul Weber <singpolyma@singpolyma.net> |
|---|---|
| date | Sat, 06 May 2023 19:40:23 -0500 |
| parent | 5420:aa068449b0b6 |
| child | 5423:5b2352dda31f |
comparison
equal
deleted
inserted
replaced
| 5672:2c69577b28c2 | 5673:0eb2d5ea2428 |
|---|---|
| 4 local jid = require "util.jid"; | 4 local jid = require "util.jid"; |
| 5 local json = require "util.json"; | 5 local json = require "util.json"; |
| 6 local usermanager = require "core.usermanager"; | 6 local usermanager = require "core.usermanager"; |
| 7 local errors = require "util.error"; | 7 local errors = require "util.error"; |
| 8 local url = require "socket.url"; | 8 local url = require "socket.url"; |
| 9 local uuid = require "util.uuid"; | 9 local id = require "util.id"; |
| 10 local encodings = require "util.encodings"; | 10 local encodings = require "util.encodings"; |
| 11 local base64 = encodings.base64; | 11 local base64 = encodings.base64; |
| 12 local random = require "util.random"; | |
| 13 local schema = require "util.jsonschema"; | |
| 14 local set = require "util.set"; | |
| 15 local jwt = require"util.jwt"; | |
| 16 local it = require "util.iterators"; | |
| 17 local array = require "util.array"; | |
| 18 local st = require "util.stanza"; | |
| 19 | |
| 20 local function b64url(s) | |
| 21 return (base64.encode(s):gsub("[+/=]", { ["+"] = "-", ["/"] = "_", ["="] = "" })) | |
| 22 end | |
| 23 | |
| 24 local function tmap(t) | |
| 25 return function(k) | |
| 26 return t[k]; | |
| 27 end | |
| 28 end | |
| 29 | |
| 30 local function read_file(base_path, fn, required) | |
| 31 local f, err = io.open(base_path .. "/" .. fn); | |
| 32 if not f then | |
| 33 module:log(required and "error" or "debug", "Unable to load template file: %s", err); | |
| 34 if required then | |
| 35 return error("Failed to load templates"); | |
| 36 end | |
| 37 return nil; | |
| 38 end | |
| 39 local data = assert(f:read("*a")); | |
| 40 assert(f:close()); | |
| 41 return data; | |
| 42 end | |
| 43 | |
| 44 local template_path = module:get_option_path("oauth2_template_path", "html"); | |
| 45 local templates = { | |
| 46 login = read_file(template_path, "login.html", true); | |
| 47 consent = read_file(template_path, "consent.html", true); | |
| 48 error = read_file(template_path, "error.html", true); | |
| 49 css = read_file(template_path, "style.css"); | |
| 50 js = read_file(template_path, "script.js"); | |
| 51 }; | |
| 52 | |
| 53 local site_name = module:get_option_string("site_name", module.host); | |
| 54 | |
| 55 local _render_html = require"util.interpolation".new("%b{}", st.xml_escape); | |
| 56 local function render_page(template, data, sensitive) | |
| 57 data = data or {}; | |
| 58 data.site_name = site_name; | |
| 59 local resp = { | |
| 60 status_code = 200; | |
| 61 headers = { | |
| 62 ["Content-Type"] = "text/html; charset=utf-8"; | |
| 63 ["Content-Security-Policy"] = "default-src 'self'"; | |
| 64 ["X-Frame-Options"] = "DENY"; | |
| 65 ["Cache-Control"] = (sensitive and "no-store" or "no-cache")..", private"; | |
| 66 }; | |
| 67 body = _render_html(template, data); | |
| 68 }; | |
| 69 return resp; | |
| 70 end | |
| 12 | 71 |
| 13 local tokens = module:depends("tokenauth"); | 72 local tokens = module:depends("tokenauth"); |
| 14 | 73 |
| 15 local clients = module:open_store("oauth2_clients", "map"); | 74 local default_access_ttl = module:get_option_number("oauth2_access_token_ttl", 86400); |
| 16 | 75 local default_refresh_ttl = module:get_option_number("oauth2_refresh_token_ttl", nil); |
| 17 local function filter_scopes(username, host, requested_scope_string) | 76 |
| 18 if host ~= module.host then | 77 -- Used to derive client_secret from client_id, set to enable stateless dynamic registration. |
| 19 return usermanager.get_jid_role(username.."@"..host, module.host).name; | 78 local registration_key = module:get_option_string("oauth2_registration_key"); |
| 20 end | 79 local registration_algo = module:get_option_string("oauth2_registration_algorithm", "HS256"); |
| 21 | 80 local registration_ttl = module:get_option("oauth2_registration_ttl", nil); |
| 22 if requested_scope_string then -- Specific role requested | 81 local registration_options = module:get_option("oauth2_registration_options", |
| 23 -- TODO: The requested scope string is technically a space-delimited list | 82 { default_ttl = registration_ttl; accept_expired = not registration_ttl }); |
| 24 -- of scopes, but for simplicity we're mapping this slot to role names. | 83 |
| 25 if usermanager.user_can_assume_role(username, module.host, requested_scope_string) then | 84 local pkce_required = module:get_option_boolean("oauth2_require_code_challenge", false); |
| 26 return requested_scope_string; | 85 |
| 27 end | 86 local verification_key; |
| 28 end | 87 local jwt_sign, jwt_verify; |
| 29 | 88 if registration_key then |
| 89 -- Tie it to the host if global | |
| 90 verification_key = hashes.hmac_sha256(registration_key, module.host); | |
| 91 jwt_sign, jwt_verify = jwt.init(registration_algo, registration_key, registration_key, registration_options); | |
| 92 end | |
| 93 | |
| 94 local function parse_scopes(scope_string) | |
| 95 return array(scope_string:gmatch("%S+")); | |
| 96 end | |
| 97 | |
| 98 local openid_claims = set.new({ "openid", "profile"; "email"; "address"; "phone" }); | |
| 99 | |
| 100 local function split_scopes(scope_list) | |
| 101 local claims, roles, unknown = array(), array(), array(); | |
| 102 local all_roles = usermanager.get_all_roles(module.host); | |
| 103 for _, scope in ipairs(scope_list) do | |
| 104 if openid_claims:contains(scope) then | |
| 105 claims:push(scope); | |
| 106 elseif all_roles[scope] then | |
| 107 roles:push(scope); | |
| 108 else | |
| 109 unknown:push(scope); | |
| 110 end | |
| 111 end | |
| 112 return claims, roles, unknown; | |
| 113 end | |
| 114 | |
| 115 local function can_assume_role(username, requested_role) | |
| 116 return usermanager.user_can_assume_role(username, module.host, requested_role); | |
| 117 end | |
| 118 | |
| 119 local function select_role(username, requested_roles) | |
| 120 if requested_roles then | |
| 121 for _, requested_role in ipairs(requested_roles) do | |
| 122 if can_assume_role(username, requested_role) then | |
| 123 return requested_role; | |
| 124 end | |
| 125 end | |
| 126 end | |
| 127 -- otherwise the default role | |
| 30 return usermanager.get_user_role(username, module.host).name; | 128 return usermanager.get_user_role(username, module.host).name; |
| 31 end | 129 end |
| 32 | 130 |
| 33 local function code_expires_in(code) | 131 local function filter_scopes(username, requested_scope_string) |
| 34 return os.difftime(os.time(), code.issued); | 132 local granted_scopes, requested_roles; |
| 35 end | 133 |
| 36 | 134 if requested_scope_string then -- Specific role(s) requested |
| 37 local function code_expired(code) | 135 granted_scopes, requested_roles = split_scopes(parse_scopes(requested_scope_string)); |
| 38 return code_expires_in(code) > 120; | 136 else |
| 137 granted_scopes = array(); | |
| 138 end | |
| 139 | |
| 140 local selected_role = select_role(username, requested_roles); | |
| 141 granted_scopes:push(selected_role); | |
| 142 | |
| 143 return granted_scopes:concat(" "), selected_role; | |
| 144 end | |
| 145 | |
| 146 local function code_expires_in(code) --> number, seconds until code expires | |
| 147 return os.difftime(code.expires, os.time()); | |
| 148 end | |
| 149 | |
| 150 local function code_expired(code) --> boolean, true: has expired, false: still valid | |
| 151 return code_expires_in(code) < 0; | |
| 39 end | 152 end |
| 40 | 153 |
| 41 local codes = cache.new(10000, function (_, code) | 154 local codes = cache.new(10000, function (_, code) |
| 42 return code_expired(code) | 155 return code_expired(code) |
| 43 end); | 156 end); |
| 44 | 157 |
| 45 module:add_timer(900, function() | 158 -- Periodically clear out unredeemed codes. Does not need to be exact, expired |
| 159 -- codes are rejected if tried. Mostly just to keep memory usage in check. | |
| 160 module:hourly("Clear expired authorization codes", function() | |
| 46 local k, code = codes:tail(); | 161 local k, code = codes:tail(); |
| 47 while code and code_expired(code) do | 162 while code and code_expired(code) do |
| 48 codes:set(k, nil); | 163 codes:set(k, nil); |
| 49 k, code = codes:tail(); | 164 k, code = codes:tail(); |
| 50 end | 165 end |
| 51 return code and code_expires_in(code) + 1 or 900; | |
| 52 end) | 166 end) |
| 167 | |
| 168 local function get_issuer() | |
| 169 return (module:http_url(nil, "/"):gsub("/$", "")); | |
| 170 end | |
| 171 | |
| 172 local loopbacks = set.new({ "localhost", "127.0.0.1", "::1" }); | |
| 173 local function is_secure_redirect(uri) | |
| 174 local u = url.parse(uri); | |
| 175 return u.scheme ~= "http" or loopbacks:contains(u.host); | |
| 176 end | |
| 53 | 177 |
| 54 local function oauth_error(err_name, err_desc) | 178 local function oauth_error(err_name, err_desc) |
| 55 return errors.new({ | 179 return errors.new({ |
| 56 type = "modify"; | 180 type = "modify"; |
| 57 condition = "bad-request"; | 181 condition = "bad-request"; |
| 59 text = err_desc and (err_name..": "..err_desc) or err_name; | 183 text = err_desc and (err_name..": "..err_desc) or err_name; |
| 60 extra = { oauth2_response = { error = err_name, error_description = err_desc } }; | 184 extra = { oauth2_response = { error = err_name, error_description = err_desc } }; |
| 61 }); | 185 }); |
| 62 end | 186 end |
| 63 | 187 |
| 64 local function new_access_token(token_jid, scope, ttl) | 188 -- client_id / client_metadata are pretty large, filter out a subset of |
| 65 local token = tokens.create_jid_token(token_jid, token_jid, scope, ttl); | 189 -- properties that are deemed useful e.g. in case tokens issued to a certain |
| 190 -- client needs to be revoked | |
| 191 local function client_subset(client) | |
| 192 return { name = client.client_name; uri = client.client_uri; id = client.software_id; version = client.software_version }; | |
| 193 end | |
| 194 | |
| 195 local function new_access_token(token_jid, role, scope_string, client, id_token, refresh_token_info) | |
| 196 local token_data = { oauth2_scopes = scope_string, oauth2_client = nil }; | |
| 197 if client then | |
| 198 token_data.oauth2_client = client_subset(client); | |
| 199 end | |
| 200 if next(token_data) == nil then | |
| 201 token_data = nil; | |
| 202 end | |
| 203 | |
| 204 local refresh_token; | |
| 205 local grant = refresh_token_info and refresh_token_info.grant; | |
| 206 if not grant then | |
| 207 -- No existing grant, create one | |
| 208 grant = tokens.create_grant(token_jid, token_jid, default_refresh_ttl, token_data); | |
| 209 -- Create refresh token for the grant if desired | |
| 210 refresh_token = refresh_token_info ~= false and tokens.create_token(token_jid, grant, nil, nil, "oauth2-refresh"); | |
| 211 else | |
| 212 -- Grant exists, reuse existing refresh token | |
| 213 refresh_token = refresh_token_info.token; | |
| 214 | |
| 215 refresh_token_info.grant = nil; -- Prevent reference loop | |
| 216 end | |
| 217 | |
| 218 local access_token, access_token_info = tokens.create_token(token_jid, grant, role, default_access_ttl, "oauth2"); | |
| 219 | |
| 220 local expires_at = access_token_info.expires; | |
| 66 return { | 221 return { |
| 67 token_type = "bearer"; | 222 token_type = "bearer"; |
| 68 access_token = token; | 223 access_token = access_token; |
| 69 expires_in = ttl; | 224 expires_in = expires_at and (expires_at - os.time()) or nil; |
| 70 scope = scope; | 225 scope = scope_string; |
| 71 -- TODO: include refresh_token when implemented | 226 id_token = id_token; |
| 227 refresh_token = refresh_token or nil; | |
| 72 }; | 228 }; |
| 229 end | |
| 230 | |
| 231 local function get_redirect_uri(client, query_redirect_uri) -- record client, string : string | |
| 232 if not query_redirect_uri then | |
| 233 if #client.redirect_uris ~= 1 then | |
| 234 -- Client registered multiple URIs, it needs specify which one to use | |
| 235 return; | |
| 236 end | |
| 237 -- When only a single URI is registered, that's the default | |
| 238 return client.redirect_uris[1]; | |
| 239 end | |
| 240 -- Verify the client-provided URI matches one previously registered | |
| 241 for _, redirect_uri in ipairs(client.redirect_uris) do | |
| 242 if query_redirect_uri == redirect_uri then | |
| 243 return redirect_uri | |
| 244 end | |
| 245 end | |
| 73 end | 246 end |
| 74 | 247 |
| 75 local grant_type_handlers = {}; | 248 local grant_type_handlers = {}; |
| 76 local response_type_handlers = {}; | 249 local response_type_handlers = {}; |
| 250 local verifier_transforms = {}; | |
| 77 | 251 |
| 78 function grant_type_handlers.password(params) | 252 function grant_type_handlers.password(params) |
| 79 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); | 253 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); |
| 80 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); | 254 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); |
| 81 local request_username, request_host, request_resource = jid.prepped_split(request_jid); | 255 local request_username, request_host, request_resource = jid.prepped_split(request_jid); |
| 86 if not usermanager.test_password(request_username, request_host, request_password) then | 260 if not usermanager.test_password(request_username, request_host, request_password) then |
| 87 return oauth_error("invalid_grant", "incorrect credentials"); | 261 return oauth_error("invalid_grant", "incorrect credentials"); |
| 88 end | 262 end |
| 89 | 263 |
| 90 local granted_jid = jid.join(request_username, request_host, request_resource); | 264 local granted_jid = jid.join(request_username, request_host, request_resource); |
| 91 local granted_scopes = filter_scopes(request_username, request_host, params.scope); | 265 local granted_scopes, granted_role = filter_scopes(request_username, params.scope); |
| 92 return json.encode(new_access_token(granted_jid, granted_scopes, nil)); | 266 return json.encode(new_access_token(granted_jid, granted_role, granted_scopes, nil)); |
| 93 end | 267 end |
| 94 | 268 |
| 95 function response_type_handlers.code(params, granted_jid) | 269 function response_type_handlers.code(client, params, granted_jid, id_token) |
| 96 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end | 270 local request_username, request_host = jid.split(granted_jid); |
| 97 if not params.redirect_uri then return oauth_error("invalid_request", "missing 'redirect_uri'"); end | 271 if not request_host or request_host ~= module.host then |
| 98 | 272 return oauth_error("invalid_request", "invalid JID"); |
| 99 local client_owner, client_host, client_id = jid.prepped_split(params.client_id); | 273 end |
| 100 if client_host ~= module.host then | 274 local granted_scopes, granted_role = filter_scopes(request_username, params.scope); |
| 101 return oauth_error("invalid_client", "incorrect credentials"); | 275 |
| 102 end | 276 if pkce_required and not params.code_challenge then |
| 103 local client, err = clients:get(client_owner, client_id); | 277 return oauth_error("invalid_request", "PKCE required"); |
| 104 if err then error(err); end | 278 end |
| 105 if not client then | 279 |
| 106 return oauth_error("invalid_client", "incorrect credentials"); | 280 local code = id.medium(); |
| 107 end | |
| 108 | |
| 109 local granted_scopes = filter_scopes(client_owner, client_host, params.scope); | |
| 110 | |
| 111 local code = uuid.generate(); | |
| 112 local ok = codes:set(params.client_id .. "#" .. code, { | 281 local ok = codes:set(params.client_id .. "#" .. code, { |
| 113 issued = os.time(); | 282 expires = os.time() + 600; |
| 114 granted_jid = granted_jid; | 283 granted_jid = granted_jid; |
| 115 granted_scopes = granted_scopes; | 284 granted_scopes = granted_scopes; |
| 285 granted_role = granted_role; | |
| 286 challenge = params.code_challenge; | |
| 287 challenge_method = params.code_challenge_method; | |
| 288 id_token = id_token; | |
| 116 }); | 289 }); |
| 117 if not ok then | 290 if not ok then |
| 118 return {status_code = 429}; | 291 return {status_code = 429}; |
| 119 end | 292 end |
| 120 | 293 |
| 121 local redirect = url.parse(params.redirect_uri); | 294 local redirect_uri = get_redirect_uri(client, params.redirect_uri); |
| 295 if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" then | |
| 296 -- TODO some nicer template page | |
| 297 -- mod_http_errors will set content-type to text/html if it catches this | |
| 298 -- event, if not text/plain is kept for the fallback text. | |
| 299 local response = { status_code = 200; headers = { content_type = "text/plain" } } | |
| 300 response.body = module:context("*"):fire_event("http-message", { | |
| 301 response = response; | |
| 302 title = "Your authorization code"; | |
| 303 message = "Here's your authorization code, copy and paste it into " .. (client.client_name or "your client"); | |
| 304 extra = code; | |
| 305 }) or ("Here's your authorization code:\n%s\n"):format(code); | |
| 306 return response; | |
| 307 elseif not redirect_uri then | |
| 308 return 400; | |
| 309 end | |
| 310 | |
| 311 local redirect = url.parse(redirect_uri); | |
| 312 | |
| 122 local query = http.formdecode(redirect.query or ""); | 313 local query = http.formdecode(redirect.query or ""); |
| 123 if type(query) ~= "table" then query = {}; end | 314 if type(query) ~= "table" then query = {}; end |
| 124 table.insert(query, { name = "code", value = code }) | 315 table.insert(query, { name = "code", value = code }); |
| 316 table.insert(query, { name = "iss", value = get_issuer() }); | |
| 125 if params.state then | 317 if params.state then |
| 126 table.insert(query, { name = "state", value = params.state }); | 318 table.insert(query, { name = "state", value = params.state }); |
| 127 end | 319 end |
| 128 redirect.query = http.formencode(query); | 320 redirect.query = http.formencode(query); |
| 129 | 321 |
| 130 return { | 322 return { |
| 131 status_code = 302; | 323 status_code = 303; |
| 132 headers = { | 324 headers = { |
| 133 location = url.build(redirect); | 325 location = url.build(redirect); |
| 134 }; | 326 }; |
| 135 } | 327 } |
| 136 end | 328 end |
| 137 | 329 |
| 138 local pepper = module:get_option_string("oauth2_client_pepper", ""); | 330 -- Implicit flow |
| 139 | 331 function response_type_handlers.token(client, params, granted_jid) |
| 140 local function verify_secret(stored, salt, i, secret) | 332 local request_username, request_host = jid.split(granted_jid); |
| 141 return base64.decode(stored) == hashes.pbkdf2_hmac_sha256(secret, salt .. pepper, i); | 333 if not request_host or request_host ~= module.host then |
| 334 return oauth_error("invalid_request", "invalid JID"); | |
| 335 end | |
| 336 local granted_scopes, granted_role = filter_scopes(request_username, params.scope); | |
| 337 local token_info = new_access_token(granted_jid, granted_role, granted_scopes, client, nil); | |
| 338 | |
| 339 local redirect = url.parse(get_redirect_uri(client, params.redirect_uri)); | |
| 340 if not redirect then return 400; end | |
| 341 token_info.state = params.state; | |
| 342 redirect.fragment = http.formencode(token_info); | |
| 343 | |
| 344 return { | |
| 345 status_code = 303; | |
| 346 headers = { | |
| 347 location = url.build(redirect); | |
| 348 }; | |
| 349 } | |
| 350 end | |
| 351 | |
| 352 local function make_client_secret(client_id) --> client_secret | |
| 353 return hashes.hmac_sha256(verification_key, client_id, true); | |
| 354 end | |
| 355 | |
| 356 local function verify_client_secret(client_id, client_secret) | |
| 357 return hashes.equals(make_client_secret(client_id), client_secret); | |
| 142 end | 358 end |
| 143 | 359 |
| 144 function grant_type_handlers.authorization_code(params) | 360 function grant_type_handlers.authorization_code(params) |
| 145 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end | 361 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end |
| 146 if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end | 362 if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end |
| 147 if not params.code then return oauth_error("invalid_request", "missing 'code'"); end | 363 if not params.code then return oauth_error("invalid_request", "missing 'code'"); end |
| 148 if params.scope and params.scope ~= "" then | 364 if params.scope and params.scope ~= "" then |
| 149 return oauth_error("invalid_scope", "unknown scope requested"); | 365 return oauth_error("invalid_scope", "unknown scope requested"); |
| 150 end | 366 end |
| 151 | 367 |
| 152 local client_owner, client_host, client_id = jid.prepped_split(params.client_id); | 368 local client_ok, client = jwt_verify(params.client_id); |
| 153 if client_host ~= module.host then | 369 if not client_ok then |
| 154 module:log("debug", "%q ~= %q", client_host, module.host); | |
| 155 return oauth_error("invalid_client", "incorrect credentials"); | 370 return oauth_error("invalid_client", "incorrect credentials"); |
| 156 end | 371 end |
| 157 local client, err = clients:get(client_owner, client_id); | 372 |
| 158 if err then error(err); end | 373 if not verify_client_secret(params.client_id, params.client_secret) then |
| 159 if not client or not verify_secret(client.secret_hash, client.salt, client.iteration_count, params.client_secret) then | |
| 160 module:log("debug", "client_secret mismatch"); | 374 module:log("debug", "client_secret mismatch"); |
| 161 return oauth_error("invalid_client", "incorrect credentials"); | 375 return oauth_error("invalid_client", "incorrect credentials"); |
| 162 end | 376 end |
| 163 local code, err = codes:get(params.client_id .. "#" .. params.code); | 377 local code, err = codes:get(params.client_id .. "#" .. params.code); |
| 164 if err then error(err); end | 378 if err then error(err); end |
| 379 -- MUST NOT use the authorization code more than once, so remove it to | |
| 380 -- prevent a second attempted use | |
| 381 codes:set(params.client_id .. "#" .. params.code, nil); | |
| 165 if not code or type(code) ~= "table" or code_expired(code) then | 382 if not code or type(code) ~= "table" or code_expired(code) then |
| 166 module:log("debug", "authorization_code invalid or expired: %q", code); | 383 module:log("debug", "authorization_code invalid or expired: %q", code); |
| 167 return oauth_error("invalid_client", "incorrect credentials"); | 384 return oauth_error("invalid_client", "incorrect credentials"); |
| 168 end | 385 end |
| 169 assert(codes:set(client_owner, client_id .. "#" .. params.code, nil)); | 386 |
| 170 | 387 -- TODO Decide if the code should be removed or not when PKCE fails |
| 171 return json.encode(new_access_token(code.granted_jid, code.granted_scopes, nil)); | 388 local transform = verifier_transforms[code.challenge_method or "plain"]; |
| 172 end | 389 if not transform then |
| 173 | 390 return oauth_error("invalid_request", "unknown challenge transform method"); |
| 174 local function check_credentials(request, allow_token) | 391 elseif transform(params.code_verifier) ~= code.challenge then |
| 392 return oauth_error("invalid_grant", "incorrect credentials"); | |
| 393 end | |
| 394 | |
| 395 return json.encode(new_access_token(code.granted_jid, code.granted_role, code.granted_scopes, client, code.id_token)); | |
| 396 end | |
| 397 | |
| 398 function grant_type_handlers.refresh_token(params) | |
| 399 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end | |
| 400 if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end | |
| 401 if not params.refresh_token then return oauth_error("invalid_request", "missing 'refresh_token'"); end | |
| 402 | |
| 403 local client_ok, client = jwt_verify(params.client_id); | |
| 404 if not client_ok then | |
| 405 return oauth_error("invalid_client", "incorrect credentials"); | |
| 406 end | |
| 407 | |
| 408 if not verify_client_secret(params.client_id, params.client_secret) then | |
| 409 module:log("debug", "client_secret mismatch"); | |
| 410 return oauth_error("invalid_client", "incorrect credentials"); | |
| 411 end | |
| 412 | |
| 413 local refresh_token_info = tokens.get_token_info(params.refresh_token); | |
| 414 if not refresh_token_info or refresh_token_info.purpose ~= "oauth2-refresh" then | |
| 415 return oauth_error("invalid_grant", "invalid refresh token"); | |
| 416 end | |
| 417 | |
| 418 -- new_access_token() requires the actual token | |
| 419 refresh_token_info.token = params.refresh_token; | |
| 420 | |
| 421 return json.encode(new_access_token( | |
| 422 refresh_token_info.jid, refresh_token_info.role, refresh_token_info.grant.data.oauth2_scopes, client, nil, refresh_token_info | |
| 423 )); | |
| 424 end | |
| 425 | |
| 426 -- RFC 7636 Proof Key for Code Exchange by OAuth Public Clients | |
| 427 | |
| 428 function verifier_transforms.plain(code_verifier) | |
| 429 -- code_challenge = code_verifier | |
| 430 return code_verifier; | |
| 431 end | |
| 432 | |
| 433 function verifier_transforms.S256(code_verifier) | |
| 434 -- code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) | |
| 435 return code_verifier and b64url(hashes.sha256(code_verifier)); | |
| 436 end | |
| 437 | |
| 438 -- Used to issue/verify short-lived tokens for the authorization process below | |
| 439 local new_user_token, verify_user_token = jwt.init("HS256", random.bytes(32), nil, { default_ttl = 600 }); | |
| 440 | |
| 441 -- From the given request, figure out if the user is authenticated and has granted consent yet | |
| 442 -- As this requires multiple steps (seek credentials, seek consent), we have a lot of state to | |
| 443 -- carry around across requests. We also need to protect against CSRF and session mix-up attacks | |
| 444 -- (e.g. the user may have multiple concurrent flows in progress, session cookies aren't unique | |
| 445 -- to one of them). | |
| 446 -- Our strategy here is to preserve the original query string (containing the authz request), and | |
| 447 -- encode the rest of the flow in form POSTs. | |
| 448 local function get_auth_state(request) | |
| 449 local form = request.method == "POST" | |
| 450 and request.body | |
| 451 and request.body ~= "" | |
| 452 and request.headers.content_type == "application/x-www-form-urlencoded" | |
| 453 and http.formdecode(request.body); | |
| 454 | |
| 455 if type(form) ~= "table" then return {}; end | |
| 456 | |
| 457 if not form.user_token then | |
| 458 -- First step: login | |
| 459 local username = encodings.stringprep.nodeprep(form.username); | |
| 460 local password = encodings.stringprep.saslprep(form.password); | |
| 461 if not (username and password) or not usermanager.test_password(username, module.host, password) then | |
| 462 return { | |
| 463 error = "Invalid username/password"; | |
| 464 }; | |
| 465 end | |
| 466 return { | |
| 467 user = { | |
| 468 username = username; | |
| 469 host = module.host; | |
| 470 token = new_user_token({ username = username, host = module.host }); | |
| 471 }; | |
| 472 }; | |
| 473 elseif form.user_token and form.consent then | |
| 474 -- Second step: consent | |
| 475 local ok, user = verify_user_token(form.user_token); | |
| 476 if not ok then | |
| 477 return { | |
| 478 error = user == "token-expired" and "Session expired - try again" or nil; | |
| 479 }; | |
| 480 end | |
| 481 | |
| 482 local scope = array():append(form):filter(function(field) | |
| 483 return field.name == "scope" or field.name == "role"; | |
| 484 end):pluck("value"):concat(" "); | |
| 485 | |
| 486 user.token = form.user_token; | |
| 487 return { | |
| 488 user = user; | |
| 489 scope = scope; | |
| 490 consent = form.consent == "granted"; | |
| 491 }; | |
| 492 end | |
| 493 | |
| 494 return {}; | |
| 495 end | |
| 496 | |
| 497 local function get_request_credentials(request) | |
| 498 if not request.headers.authorization then return; end | |
| 499 | |
| 175 local auth_type, auth_data = string.match(request.headers.authorization, "^(%S+)%s(.+)$"); | 500 local auth_type, auth_data = string.match(request.headers.authorization, "^(%S+)%s(.+)$"); |
| 176 | 501 |
| 177 if auth_type == "Basic" then | 502 if auth_type == "Basic" then |
| 178 local creds = base64.decode(auth_data); | 503 local creds = base64.decode(auth_data); |
| 179 if not creds then return false; end | 504 if not creds then return; end |
| 180 local username, password = string.match(creds, "^([^:]+):(.*)$"); | 505 local username, password = string.match(creds, "^([^:]+):(.*)$"); |
| 181 if not username then return false; end | 506 if not username then return; end |
| 182 username, password = encodings.stringprep.nodeprep(username), encodings.stringprep.saslprep(password); | 507 return { |
| 183 if not username then return false; end | 508 type = "basic"; |
| 184 if not usermanager.test_password(username, module.host, password) then | 509 username = username; |
| 185 return false; | 510 password = password; |
| 186 end | 511 }; |
| 187 return username; | 512 elseif auth_type == "Bearer" then |
| 188 elseif auth_type == "Bearer" and allow_token then | 513 return { |
| 189 local token_info = tokens.get_token_info(auth_data); | 514 type = "bearer"; |
| 190 if not token_info or not token_info.session or token_info.session.host ~= module.host then | 515 bearer_token = auth_data; |
| 191 return false; | 516 }; |
| 192 end | 517 end |
| 193 return token_info.session.username; | 518 |
| 194 end | |
| 195 return nil; | 519 return nil; |
| 196 end | 520 end |
| 197 | 521 |
| 198 if module:get_host_type() == "component" then | 522 if module:get_host_type() == "component" then |
| 199 local component_secret = assert(module:get_option_string("component_secret"), "'component_secret' is a required setting when loaded on a Component"); | 523 local component_secret = assert(module:get_option_string("component_secret"), "'component_secret' is a required setting when loaded on a Component"); |
| 208 if not request_host or request_host ~= module.host then | 532 if not request_host or request_host ~= module.host then |
| 209 return oauth_error("invalid_request", "invalid JID"); | 533 return oauth_error("invalid_request", "invalid JID"); |
| 210 end | 534 end |
| 211 if request_password == component_secret then | 535 if request_password == component_secret then |
| 212 local granted_jid = jid.join(request_username, request_host, request_resource); | 536 local granted_jid = jid.join(request_username, request_host, request_resource); |
| 213 return json.encode(new_access_token(granted_jid, nil, nil)); | 537 return json.encode(new_access_token(granted_jid, nil, nil, nil)); |
| 214 end | 538 end |
| 215 return oauth_error("invalid_grant", "incorrect credentials"); | 539 return oauth_error("invalid_grant", "incorrect credentials"); |
| 216 end | 540 end |
| 217 | 541 |
| 218 -- TODO How would this make sense with components? | 542 -- TODO How would this make sense with components? |
| 219 -- Have an admin authenticate maybe? | 543 -- Have an admin authenticate maybe? |
| 220 response_type_handlers.code = nil; | 544 response_type_handlers.code = nil; |
| 545 response_type_handlers.token = nil; | |
| 221 grant_type_handlers.authorization_code = nil; | 546 grant_type_handlers.authorization_code = nil; |
| 222 check_credentials = function () return false end | 547 end |
| 548 | |
| 549 -- OAuth errors should be returned to the client if possible, i.e. by | |
| 550 -- appending the error information to the redirect_uri and sending the | |
| 551 -- redirect to the user-agent. In some cases we can't do this, e.g. if | |
| 552 -- the redirect_uri is missing or invalid. In those cases, we render an | |
| 553 -- error directly to the user-agent. | |
| 554 local function error_response(request, err) | |
| 555 local q = request.url.query and http.formdecode(request.url.query); | |
| 556 local redirect_uri = q and q.redirect_uri; | |
| 557 if not redirect_uri or not is_secure_redirect(redirect_uri) then | |
| 558 module:log("warn", "Missing or invalid redirect_uri <%s>, rendering error to user-agent", redirect_uri or ""); | |
| 559 return render_page(templates.error, { error = err }); | |
| 560 end | |
| 561 local redirect_query = url.parse(redirect_uri); | |
| 562 local sep = redirect_query.query and "&" or "?"; | |
| 563 redirect_uri = redirect_uri | |
| 564 .. sep .. http.formencode(err.extra.oauth2_response) | |
| 565 .. "&" .. http.formencode({ state = q.state, iss = get_issuer() }); | |
| 566 module:log("warn", "Sending error response to client via redirect to %s", redirect_uri); | |
| 567 return { | |
| 568 status_code = 303; | |
| 569 headers = { | |
| 570 location = redirect_uri; | |
| 571 }; | |
| 572 }; | |
| 573 end | |
| 574 | |
| 575 local allowed_grant_type_handlers = module:get_option_set("allowed_oauth2_grant_types", {"authorization_code", "password", "refresh_token"}) | |
| 576 for handler_type in pairs(grant_type_handlers) do | |
| 577 if not allowed_grant_type_handlers:contains(handler_type) then | |
| 578 module:log("debug", "Grant type %q disabled", handler_type); | |
| 579 grant_type_handlers[handler_type] = nil; | |
| 580 else | |
| 581 module:log("debug", "Grant type %q enabled", handler_type); | |
| 582 end | |
| 583 end | |
| 584 | |
| 585 -- "token" aka implicit flow is considered insecure | |
| 586 local allowed_response_type_handlers = module:get_option_set("allowed_oauth2_response_types", {"code"}) | |
| 587 for handler_type in pairs(response_type_handlers) do | |
| 588 if not allowed_response_type_handlers:contains(handler_type) then | |
| 589 module:log("debug", "Response type %q disabled", handler_type); | |
| 590 response_type_handlers[handler_type] = nil; | |
| 591 else | |
| 592 module:log("debug", "Response type %q enabled", handler_type); | |
| 593 end | |
| 594 end | |
| 595 | |
| 596 local allowed_challenge_methods = module:get_option_set("allowed_oauth2_code_challenge_methods", { "plain"; "S256" }) | |
| 597 for handler_type in pairs(verifier_transforms) do | |
| 598 if not allowed_challenge_methods:contains(handler_type) then | |
| 599 module:log("debug", "Challenge method %q disabled", handler_type); | |
| 600 verifier_transforms[handler_type] = nil; | |
| 601 else | |
| 602 module:log("debug", "Challenge method %q enabled", handler_type); | |
| 603 end | |
| 223 end | 604 end |
| 224 | 605 |
| 225 function handle_token_grant(event) | 606 function handle_token_grant(event) |
| 607 local credentials = get_request_credentials(event.request); | |
| 608 | |
| 226 event.response.headers.content_type = "application/json"; | 609 event.response.headers.content_type = "application/json"; |
| 227 local params = http.formdecode(event.request.body); | 610 local params = http.formdecode(event.request.body); |
| 228 if not params then | 611 if not params then |
| 229 return oauth_error("invalid_request"); | 612 return error_response(event.request, oauth_error("invalid_request")); |
| 230 end | 613 end |
| 614 | |
| 615 if credentials and credentials.type == "basic" then | |
| 616 -- client_secret_basic converted internally to client_secret_post | |
| 617 params.client_id = http.urldecode(credentials.username); | |
| 618 params.client_secret = http.urldecode(credentials.password); | |
| 619 end | |
| 620 | |
| 231 local grant_type = params.grant_type | 621 local grant_type = params.grant_type |
| 232 local grant_handler = grant_type_handlers[grant_type]; | 622 local grant_handler = grant_type_handlers[grant_type]; |
| 233 if not grant_handler then | 623 if not grant_handler then |
| 234 return oauth_error("unsupported_grant_type"); | 624 return error_response(event.request, oauth_error("unsupported_grant_type")); |
| 235 end | 625 end |
| 236 return grant_handler(params); | 626 return grant_handler(params); |
| 237 end | 627 end |
| 238 | 628 |
| 239 local function handle_authorization_request(event) | 629 local function handle_authorization_request(event) |
| 240 local request, response = event.request, event.response; | 630 local request = event.request; |
| 241 if not request.headers.authorization then | 631 |
| 242 response.headers.www_authenticate = string.format("Basic realm=%q", module.host.."/"..module.name); | |
| 243 return 401; | |
| 244 end | |
| 245 local user = check_credentials(request); | |
| 246 if not user then | |
| 247 return 401; | |
| 248 end | |
| 249 -- TODO ask user for consent here | |
| 250 if not request.url.query then | 632 if not request.url.query then |
| 251 response.headers.content_type = "application/json"; | 633 return error_response(request, oauth_error("invalid_request")); |
| 252 return oauth_error("invalid_request"); | |
| 253 end | 634 end |
| 254 local params = http.formdecode(request.url.query); | 635 local params = http.formdecode(request.url.query); |
| 255 if not params then | 636 if not params then |
| 256 return oauth_error("invalid_request"); | 637 return error_response(request, oauth_error("invalid_request")); |
| 257 end | 638 end |
| 639 | |
| 640 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end | |
| 641 | |
| 642 local ok, client = jwt_verify(params.client_id); | |
| 643 | |
| 644 if not ok then | |
| 645 return oauth_error("invalid_client", "incorrect credentials"); | |
| 646 end | |
| 647 | |
| 648 local client_response_types = set.new(array(client.response_types or { "code" })); | |
| 649 client_response_types = set.intersection(client_response_types, allowed_response_type_handlers); | |
| 650 if not client_response_types:contains(params.response_type) then | |
| 651 return oauth_error("invalid_client", "response_type not allowed"); | |
| 652 end | |
| 653 | |
| 654 local auth_state = get_auth_state(request); | |
| 655 if not auth_state.user then | |
| 656 -- Render login page | |
| 657 return render_page(templates.login, { state = auth_state, client = client }); | |
| 658 elseif auth_state.consent == nil then | |
| 659 -- Render consent page | |
| 660 local scopes, requested_roles = split_scopes(parse_scopes(params.scope or "")); | |
| 661 local default_role = select_role(auth_state.user.username, requested_roles); | |
| 662 local roles = array(it.values(usermanager.get_all_roles(module.host))):filter(function(role) | |
| 663 return can_assume_role(auth_state.user.username, role.name); | |
| 664 end):sort(function(a, b) | |
| 665 return (a.priority or 0) < (b.priority or 0) | |
| 666 end):map(function(role) | |
| 667 return { name = role.name; selected = role.name == default_role }; | |
| 668 end); | |
| 669 if not roles[2] then | |
| 670 -- Only one role to choose from, might as well skip the selector | |
| 671 roles = nil; | |
| 672 end | |
| 673 return render_page(templates.consent, { state = auth_state; client = client; scopes = scopes; roles = roles }, true); | |
| 674 elseif not auth_state.consent then | |
| 675 -- Notify client of rejection | |
| 676 return error_response(request, oauth_error("access_denied")); | |
| 677 end | |
| 678 -- else auth_state.consent == true | |
| 679 | |
| 680 params.scope = auth_state.scope; | |
| 681 | |
| 682 local user_jid = jid.join(auth_state.user.username, module.host); | |
| 683 local client_secret = make_client_secret(params.client_id); | |
| 684 local id_token_signer = jwt.new_signer("HS256", client_secret); | |
| 685 local id_token = id_token_signer({ | |
| 686 iss = get_issuer(); | |
| 687 sub = url.build({ scheme = "xmpp"; path = user_jid }); | |
| 688 aud = params.client_id; | |
| 689 nonce = params.nonce; | |
| 690 }); | |
| 258 local response_type = params.response_type; | 691 local response_type = params.response_type; |
| 259 local response_handler = response_type_handlers[response_type]; | 692 local response_handler = response_type_handlers[response_type]; |
| 260 if not response_handler then | 693 if not response_handler then |
| 261 response.headers.content_type = "application/json"; | 694 return error_response(request, oauth_error("unsupported_response_type")); |
| 262 return oauth_error("unsupported_response_type"); | 695 end |
| 263 end | 696 return response_handler(client, params, user_jid, id_token); |
| 264 return response_handler(params, jid.join(user, module.host)); | |
| 265 end | 697 end |
| 266 | 698 |
| 267 local function handle_revocation_request(event) | 699 local function handle_revocation_request(event) |
| 268 local request, response = event.request, event.response; | 700 local request, response = event.request, event.response; |
| 269 if not request.headers.authorization then | 701 if request.headers.authorization then |
| 270 response.headers.www_authenticate = string.format("Basic realm=%q", module.host.."/"..module.name); | 702 local credentials = get_request_credentials(request); |
| 271 return 401; | 703 if not credentials or credentials.type ~= "basic" then |
| 272 elseif request.headers.content_type ~= "application/x-www-form-urlencoded" | 704 response.headers.www_authenticate = string.format("Basic realm=%q", module.host.."/"..module.name); |
| 273 or not request.body or request.body == "" then | 705 return 401; |
| 274 return 400; | 706 end |
| 275 end | 707 -- OAuth "client" credentials |
| 276 local user = check_credentials(request, true); | 708 if not verify_client_secret(credentials.username, credentials.password) then |
| 277 if not user then | 709 return 401; |
| 278 return 401; | 710 end |
| 279 end | 711 end |
| 280 | 712 |
| 281 local form_data = http.formdecode(event.request.body); | 713 local form_data = http.formdecode(event.request.body or ""); |
| 282 if not form_data or not form_data.token then | 714 if not form_data or not form_data.token then |
| 283 return 400; | 715 response.headers.accept = "application/x-www-form-urlencoded"; |
| 716 return 415; | |
| 284 end | 717 end |
| 285 local ok, err = tokens.revoke_token(form_data.token); | 718 local ok, err = tokens.revoke_token(form_data.token); |
| 286 if not ok then | 719 if not ok then |
| 287 module:log("warn", "Unable to revoke token: %s", tostring(err)); | 720 module:log("warn", "Unable to revoke token: %s", tostring(err)); |
| 288 return 500; | 721 return 500; |
| 289 end | 722 end |
| 290 return 200; | 723 return 200; |
| 291 end | 724 end |
| 292 | 725 |
| 726 local registration_schema = { | |
| 727 type = "object"; | |
| 728 required = { | |
| 729 -- These are shown to users in the template | |
| 730 "client_name"; | |
| 731 "client_uri"; | |
| 732 -- We need at least one redirect URI for things to work | |
| 733 "redirect_uris"; | |
| 734 }; | |
| 735 properties = { | |
| 736 redirect_uris = { type = "array"; minLength = 1; items = { type = "string"; format = "uri" } }; | |
| 737 token_endpoint_auth_method = { | |
| 738 type = "string"; | |
| 739 enum = { "none"; "client_secret_post"; "client_secret_basic" }; | |
| 740 default = "client_secret_basic"; | |
| 741 }; | |
| 742 grant_types = { | |
| 743 type = "array"; | |
| 744 items = { | |
| 745 type = "string"; | |
| 746 enum = { | |
| 747 "authorization_code"; | |
| 748 "implicit"; | |
| 749 "password"; | |
| 750 "client_credentials"; | |
| 751 "refresh_token"; | |
| 752 "urn:ietf:params:oauth:grant-type:jwt-bearer"; | |
| 753 "urn:ietf:params:oauth:grant-type:saml2-bearer"; | |
| 754 }; | |
| 755 }; | |
| 756 default = { "authorization_code" }; | |
| 757 }; | |
| 758 application_type = { type = "string"; enum = { "native"; "web" }; default = "web" }; | |
| 759 response_types = { type = "array"; items = { type = "string"; enum = { "code"; "token" } }; default = { "code" } }; | |
| 760 client_name = { type = "string" }; | |
| 761 client_uri = { type = "string"; format = "uri"; luaPattern = "^https:" }; | |
| 762 logo_uri = { type = "string"; format = "uri"; luaPattern = "^https:" }; | |
| 763 scope = { type = "string" }; | |
| 764 contacts = { type = "array"; items = { type = "string"; format = "email" } }; | |
| 765 tos_uri = { type = "string"; format = "uri"; luaPattern = "^https:" }; | |
| 766 policy_uri = { type = "string"; format = "uri"; luaPattern = "^https:" }; | |
| 767 jwks_uri = { type = "string"; format = "uri"; luaPattern = "^https:" }; | |
| 768 jwks = { type = "object"; description = "JSON Web Key Set, RFC 7517" }; | |
| 769 software_id = { type = "string"; format = "uuid" }; | |
| 770 software_version = { type = "string" }; | |
| 771 }; | |
| 772 luaPatternProperties = { | |
| 773 -- Localized versions of descriptive properties and URIs | |
| 774 ["^client_name#"] = { description = "Localized version of 'client_name'"; type = "string" }; | |
| 775 ["^[a-z_]+_uri#"] = { type = "string"; format = "uri"; luaPattern = "^https:" }; | |
| 776 }; | |
| 777 } | |
| 778 | |
| 779 local function redirect_uri_allowed(redirect_uri, client_uri, app_type) | |
| 780 local uri = url.parse(redirect_uri); | |
| 781 if app_type == "native" then | |
| 782 return uri.scheme == "http" and loopbacks:contains(uri.host) or uri.scheme ~= "https"; | |
| 783 elseif app_type == "web" then | |
| 784 return uri.scheme == "https" and uri.host == client_uri.host; | |
| 785 end | |
| 786 end | |
| 787 | |
| 788 function create_client(client_metadata) | |
| 789 if not schema.validate(registration_schema, client_metadata) then | |
| 790 return nil, oauth_error("invalid_request", "Failed schema validation."); | |
| 791 end | |
| 792 | |
| 793 -- Fill in default values | |
| 794 for propname, propspec in pairs(registration_schema.properties) do | |
| 795 if client_metadata[propname] == nil and type(propspec) == "table" and propspec.default ~= nil then | |
| 796 client_metadata[propname] = propspec.default; | |
| 797 end | |
| 798 end | |
| 799 | |
| 800 local client_uri = url.parse(client_metadata.client_uri); | |
| 801 if not client_uri or client_uri.scheme ~= "https" or loopbacks:contains(client_uri.host) then | |
| 802 return nil, oauth_error("invalid_client_metadata", "Missing, invalid or insecure client_uri"); | |
| 803 end | |
| 804 | |
| 805 for _, redirect_uri in ipairs(client_metadata.redirect_uris) do | |
| 806 if not redirect_uri_allowed(redirect_uri, client_uri, client_metadata.application_type) then | |
| 807 return nil, oauth_error("invalid_redirect_uri", "Invalid, insecure or inappropriate redirect URI."); | |
| 808 end | |
| 809 end | |
| 810 | |
| 811 for field, prop_schema in pairs(registration_schema.properties) do | |
| 812 if field ~= "client_uri" and prop_schema.format == "uri" and client_metadata[field] then | |
| 813 if not redirect_uri_allowed(client_metadata[field], client_uri, "web") then | |
| 814 return nil, oauth_error("invalid_client_metadata", "Invalid, insecure or inappropriate informative URI"); | |
| 815 end | |
| 816 end | |
| 817 end | |
| 818 | |
| 819 for k, v in pairs(client_metadata) do | |
| 820 local base_k = k:match"^([^#]+)#" or k; | |
| 821 if not registration_schema.properties[base_k] or k:find"^client_uri#" then | |
| 822 -- Ignore and strip unknown extra properties | |
| 823 client_metadata[k] = nil; | |
| 824 elseif k:find"_uri#" then | |
| 825 -- Localized URIs should be secure too | |
| 826 if not redirect_uri_allowed(v, client_uri, "web") then | |
| 827 return nil, oauth_error("invalid_client_metadata", "Invalid, insecure or inappropriate informative URI"); | |
| 828 end | |
| 829 end | |
| 830 end | |
| 831 | |
| 832 local grant_types = set.new(client_metadata.grant_types); | |
| 833 local response_types = set.new(client_metadata.response_types); | |
| 834 | |
| 835 if grant_types:contains("authorization_code") and not response_types:contains("code") then | |
| 836 return nil, oauth_error("invalid_client_metadata", "Inconsistency between 'grant_types' and 'response_types'"); | |
| 837 elseif grant_types:contains("implicit") and not response_types:contains("token") then | |
| 838 return nil, oauth_error("invalid_client_metadata", "Inconsistency between 'grant_types' and 'response_types'"); | |
| 839 end | |
| 840 | |
| 841 if set.intersection(grant_types, allowed_grant_type_handlers):empty() then | |
| 842 return nil, oauth_error("invalid_client_metadata", "No allowed 'grant_types' specified"); | |
| 843 elseif set.intersection(response_types, allowed_response_type_handlers):empty() then | |
| 844 return nil, oauth_error("invalid_client_metadata", "No allowed 'response_types' specified"); | |
| 845 end | |
| 846 | |
| 847 -- Ensure each signed client_id JWT is unique, short ID and issued at | |
| 848 -- timestamp should be sufficient to rule out brute force attacks | |
| 849 client_metadata.nonce = id.short(); | |
| 850 | |
| 851 -- Do we want to keep everything? | |
| 852 local client_id = jwt_sign(client_metadata); | |
| 853 | |
| 854 client_metadata.client_id = client_id; | |
| 855 client_metadata.client_id_issued_at = os.time(); | |
| 856 | |
| 857 if client_metadata.token_endpoint_auth_method ~= "none" then | |
| 858 local client_secret = make_client_secret(client_id); | |
| 859 client_metadata.client_secret = client_secret; | |
| 860 client_metadata.client_secret_expires_at = 0; | |
| 861 | |
| 862 if not registration_options.accept_expired then | |
| 863 client_metadata.client_secret_expires_at = client_metadata.client_id_issued_at + (registration_options.default_ttl or 3600); | |
| 864 end | |
| 865 end | |
| 866 | |
| 867 return client_metadata; | |
| 868 end | |
| 869 | |
| 870 local function handle_register_request(event) | |
| 871 local request = event.request; | |
| 872 local client_metadata, err = json.decode(request.body); | |
| 873 if err then | |
| 874 return oauth_error("invalid_request", "Invalid JSON"); | |
| 875 end | |
| 876 | |
| 877 local response, err = create_client(client_metadata); | |
| 878 if err then return err end | |
| 879 | |
| 880 return { | |
| 881 status_code = 201; | |
| 882 headers = { content_type = "application/json" }; | |
| 883 body = json.encode(response); | |
| 884 }; | |
| 885 end | |
| 886 | |
| 887 if not registration_key then | |
| 888 module:log("info", "No 'oauth2_registration_key', dynamic client registration disabled") | |
| 889 handle_authorization_request = nil | |
| 890 handle_register_request = nil | |
| 891 end | |
| 892 | |
| 893 local function handle_userinfo_request(event) | |
| 894 local request = event.request; | |
| 895 local credentials = get_request_credentials(request); | |
| 896 if not credentials or not credentials.bearer_token then | |
| 897 module:log("debug", "Missing credentials for UserInfo endpoint: %q", credentials) | |
| 898 return 401; | |
| 899 end | |
| 900 local token_info,err = tokens.get_token_info(credentials.bearer_token); | |
| 901 if not token_info then | |
| 902 module:log("debug", "UserInfo query failed token validation: %s", err) | |
| 903 return 403; | |
| 904 end | |
| 905 local scopes = set.new() | |
| 906 if type(token_info.grant.data) == "table" and type(token_info.grant.data.oauth2_scopes) == "string" then | |
| 907 scopes:add_list(parse_scopes(token_info.grant.data.oauth2_scopes)); | |
| 908 else | |
| 909 module:log("debug", "token_info = %q", token_info) | |
| 910 end | |
| 911 | |
| 912 if not scopes:contains("openid") then | |
| 913 module:log("debug", "Missing the 'openid' scope in %q", scopes) | |
| 914 -- The 'openid' scope is required for access to this endpoint. | |
| 915 return 403; | |
| 916 end | |
| 917 | |
| 918 local user_info = { | |
| 919 iss = get_issuer(); | |
| 920 sub = url.build({ scheme = "xmpp"; path = token_info.jid }); | |
| 921 } | |
| 922 | |
| 923 local token_claims = set.intersection(openid_claims, scopes); | |
| 924 token_claims:remove("openid"); -- that's "iss" and "sub" above | |
| 925 if not token_claims:empty() then | |
| 926 -- Another module can do that | |
| 927 module:fire_event("token/userinfo", { | |
| 928 token = token_info; | |
| 929 claims = token_claims; | |
| 930 username = jid.split(token_info.jid); | |
| 931 userinfo = user_info; | |
| 932 }); | |
| 933 end | |
| 934 | |
| 935 return { | |
| 936 status_code = 200; | |
| 937 headers = { content_type = "application/json" }; | |
| 938 body = json.encode(user_info); | |
| 939 }; | |
| 940 end | |
| 941 | |
| 293 module:depends("http"); | 942 module:depends("http"); |
| 294 module:provides("http", { | 943 module:provides("http", { |
| 295 route = { | 944 route = { |
| 945 -- OAuth 2.0 in 5 simple steps! | |
| 946 -- This is the normal 'authorization_code' flow. | |
| 947 | |
| 948 -- Step 1. Create OAuth client | |
| 949 ["POST /register"] = handle_register_request; | |
| 950 | |
| 951 -- Step 2. User-facing login and consent view | |
| 952 ["GET /authorize"] = handle_authorization_request; | |
| 953 ["POST /authorize"] = handle_authorization_request; | |
| 954 | |
| 955 -- Step 3. User is redirected to the 'redirect_uri' along with an | |
| 956 -- authorization code. In the insecure 'implicit' flow, the access token | |
| 957 -- is delivered here. | |
| 958 | |
| 959 -- Step 4. Retrieve access token using the code. | |
| 296 ["POST /token"] = handle_token_grant; | 960 ["POST /token"] = handle_token_grant; |
| 297 ["GET /authorize"] = handle_authorization_request; | 961 |
| 962 -- Step 4 is later repeated using the refresh token to get new access tokens. | |
| 963 | |
| 964 -- Step 5. Revoke token (access or refresh) | |
| 298 ["POST /revoke"] = handle_revocation_request; | 965 ["POST /revoke"] = handle_revocation_request; |
| 966 | |
| 967 -- OpenID | |
| 968 ["GET /userinfo"] = handle_userinfo_request; | |
| 969 | |
| 970 -- Optional static content for templates | |
| 971 ["GET /style.css"] = templates.css and { | |
| 972 headers = { | |
| 973 ["Content-Type"] = "text/css"; | |
| 974 }; | |
| 975 body = _render_html(templates.css, module:get_option("oauth2_template_style")); | |
| 976 } or nil; | |
| 977 ["GET /script.js"] = templates.js and { | |
| 978 headers = { | |
| 979 ["Content-Type"] = "text/javascript"; | |
| 980 }; | |
| 981 body = templates.js; | |
| 982 } or nil; | |
| 983 | |
| 984 -- Some convenient fallback handlers | |
| 985 ["GET /register"] = { headers = { content_type = "application/schema+json" }; body = json.encode(registration_schema) }; | |
| 986 ["GET /token"] = function() return 405; end; | |
| 987 ["GET /revoke"] = function() return 405; end; | |
| 299 }; | 988 }; |
| 300 }); | 989 }); |
| 301 | 990 |
| 302 local http_server = require "net.http.server"; | 991 local http_server = require "net.http.server"; |
| 303 | 992 |
| 308 end | 997 end |
| 309 event.response.headers.content_type = "application/json"; | 998 event.response.headers.content_type = "application/json"; |
| 310 event.response.status_code = event.error.code or 400; | 999 event.response.status_code = event.error.code or 400; |
| 311 return json.encode(oauth2_response); | 1000 return json.encode(oauth2_response); |
| 312 end, 5); | 1001 end, 5); |
| 1002 | |
| 1003 -- OIDC Discovery | |
| 1004 | |
| 1005 module:provides("http", { | |
| 1006 name = "oauth2-discovery"; | |
| 1007 default_path = "/.well-known/oauth-authorization-server"; | |
| 1008 route = { | |
| 1009 ["GET"] = { | |
| 1010 headers = { content_type = "application/json" }; | |
| 1011 body = json.encode { | |
| 1012 -- RFC 8414: OAuth 2.0 Authorization Server Metadata | |
| 1013 issuer = get_issuer(); | |
| 1014 authorization_endpoint = handle_authorization_request and module:http_url() .. "/authorize" or nil; | |
| 1015 token_endpoint = handle_token_grant and module:http_url() .. "/token" or nil; | |
| 1016 jwks_uri = nil; -- TODO? | |
| 1017 registration_endpoint = handle_register_request and module:http_url() .. "/register" or nil; | |
| 1018 scopes_supported = usermanager.get_all_roles and array(it.keys(usermanager.get_all_roles(module.host))):append(array(openid_claims:items())); | |
| 1019 response_types_supported = array(it.keys(response_type_handlers)); | |
| 1020 token_endpoint_auth_methods_supported = array({ "client_secret_post"; "client_secret_basic" }); | |
| 1021 op_policy_uri = module:get_option_string("oauth2_policy_url", nil); | |
| 1022 op_tos_uri = module:get_option_string("oauth2_terms_url", nil); | |
| 1023 revocation_endpoint = handle_revocation_request and module:http_url() .. "/revoke" or nil; | |
| 1024 revocation_endpoint_auth_methods_supported = array({ "client_secret_basic" }); | |
| 1025 code_challenge_methods_supported = array(it.keys(verifier_transforms)); | |
| 1026 grant_types_supported = array(it.keys(response_type_handlers)):map(tmap { token = "implicit"; code = "authorization_code" }); | |
| 1027 response_modes_supported = array(it.keys(response_type_handlers)):map(tmap { token = "fragment"; code = "query" }); | |
| 1028 authorization_response_iss_parameter_supported = true; | |
| 1029 service_documentation = module:get_option_string("oauth2_service_documentation", "https://modules.prosody.im/mod_http_oauth2.html"); | |
| 1030 | |
| 1031 -- OpenID | |
| 1032 userinfo_endpoint = handle_register_request and module:http_url() .. "/userinfo" or nil; | |
| 1033 id_token_signing_alg_values_supported = { "HS256" }; | |
| 1034 }; | |
| 1035 }; | |
| 1036 }; | |
| 1037 }); | |
| 1038 | |
| 1039 module:shared("tokenauth/oauthbearer_config").oidc_discovery_url = module:http_url("oauth2-discovery", "/.well-known/oauth-authorization-server"); |