Mercurial > prosody-modules
comparison mod_auth_oauth_external/mod_auth_oauth_external.lua @ 5653:62c6e17a5e9d
Merge
| author | Stephen Paul Weber <singpolyma@singpolyma.net> |
|---|---|
| date | Mon, 18 Sep 2023 08:24:19 -0500 |
| parents | 4e79f344ae2f |
| children | 0207fd248480 |
comparison
equal
deleted
inserted
replaced
| 5652:eade7ff9f52c | 5653:62c6e17a5e9d |
|---|---|
| 1 local http = require "net.http"; | 1 local http = require "net.http"; |
| 2 local async = require "util.async"; | 2 local async = require "util.async"; |
| 3 local jid = require "util.jid"; | |
| 3 local json = require "util.json"; | 4 local json = require "util.json"; |
| 4 local sasl = require "util.sasl"; | 5 local sasl = require "util.sasl"; |
| 5 | 6 |
| 6 local issuer_identity = module:get_option_string("oauth_external_issuer"); | 7 local issuer_identity = module:get_option_string("oauth_external_issuer"); |
| 7 local oidc_discovery_url = module:get_option_string("oauth_external_discovery_url", | 8 local oidc_discovery_url = module:get_option_string("oauth_external_discovery_url", |
| 13 local allow_plain = module:get_option_boolean("oauth_external_resource_owner_password", true); | 14 local allow_plain = module:get_option_boolean("oauth_external_resource_owner_password", true); |
| 14 | 15 |
| 15 -- XXX Hold up, does whatever done here even need any of these things? Are we | 16 -- XXX Hold up, does whatever done here even need any of these things? Are we |
| 16 -- the OAuth client? Is the XMPP client the OAuth client? What are we??? | 17 -- the OAuth client? Is the XMPP client the OAuth client? What are we??? |
| 17 local client_id = module:get_option_string("oauth_external_client_id"); | 18 local client_id = module:get_option_string("oauth_external_client_id"); |
| 18 -- TODO -- local client_secret = module:get_option_string("oauth_external_client_secret"); | 19 local client_secret = module:get_option_string("oauth_external_client_secret"); |
| 20 local scope = module:get_option_string("oauth_external_scope", "openid"); | |
| 19 | 21 |
| 20 --[[ More or less required endpoints | 22 --[[ More or less required endpoints |
| 21 digraph "oauth endpoints" { | 23 digraph "oauth endpoints" { |
| 22 issuer -> discovery -> { registration validation } | 24 issuer -> discovery -> { registration validation } |
| 23 registration -> { client_id client_secret } | 25 registration -> { client_id client_secret } |
| 26 --]] | 28 --]] |
| 27 | 29 |
| 28 local host = module.host; | 30 local host = module.host; |
| 29 local provider = {}; | 31 local provider = {}; |
| 30 | 32 |
| 33 local function not_implemented() | |
| 34 return nil, "method not implemented" | |
| 35 end | |
| 36 | |
| 37 -- With proper OAuth 2, most of these should be handled at the atuhorization | |
| 38 -- server, no there. | |
| 39 provider.test_password = not_implemented; | |
| 40 provider.get_password = not_implemented; | |
| 41 provider.set_password = not_implemented; | |
| 42 provider.create_user = not_implemented; | |
| 43 provider.delete_user = not_implemented; | |
| 44 | |
| 45 function provider.user_exists(_username) | |
| 46 -- Can this even be done in a generic way in OAuth 2? | |
| 47 -- OIDC and WebFinger perhaps? | |
| 48 return true; | |
| 49 end | |
| 50 | |
| 51 function provider.users() | |
| 52 -- TODO this could be done by recording known users locally | |
| 53 return function () | |
| 54 module:log("debug", "User iteration not supported"); | |
| 55 return nil; | |
| 56 end | |
| 57 end | |
| 58 | |
| 31 function provider.get_sasl_handler() | 59 function provider.get_sasl_handler() |
| 32 local profile = {}; | 60 local profile = {}; |
| 33 profile.http_client = http.default; -- TODO configurable | 61 profile.http_client = http.default; -- TODO configurable |
| 34 local extra = { oidc_discovery_url = oidc_discovery_url }; | 62 local extra = { oidc_discovery_url = oidc_discovery_url }; |
| 35 if token_endpoint and allow_plain then | 63 if token_endpoint and allow_plain then |
| 36 local map_username = function (username, _realm) return username; end; --jid.join; -- TODO configurable | 64 local map_username = function (username, _realm) return username; end; --jid.join; -- TODO configurable |
| 37 function profile:plain_test(username, password, realm) | 65 function profile:plain_test(username, password, realm) |
| 66 username = jid.unescape(username); -- COMPAT Mastodon | |
| 38 local tok, err = async.wait_for(self.profile.http_client:request(token_endpoint, { | 67 local tok, err = async.wait_for(self.profile.http_client:request(token_endpoint, { |
| 39 headers = { ["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"; ["Accept"] = "application/json" }; | 68 headers = { ["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"; ["Accept"] = "application/json" }; |
| 40 body = http.formencode({ | 69 body = http.formencode({ |
| 41 grant_type = "password"; | 70 grant_type = "password"; |
| 42 client_id = client_id; | 71 client_id = client_id; |
| 72 client_secret = client_secret; | |
| 43 username = map_username(username, realm); | 73 username = map_username(username, realm); |
| 44 password = password; | 74 password = password; |
| 45 scope = "openid"; | 75 scope = scope; |
| 46 }); | 76 }); |
| 47 })) | 77 })) |
| 48 if err or not (tok.code >= 200 and tok.code < 300) then | 78 if err or not (tok.code >= 200 and tok.code < 300) then |
| 49 return false, nil; | 79 return false, nil; |
| 50 end | 80 end |
| 51 local token_resp = json.decode(tok.body); | 81 local token_resp = json.decode(tok.body); |
| 52 if not token_resp or string.lower(token_resp.token_type or "") ~= "bearer" then | 82 if not token_resp or string.lower(token_resp.token_type or "") ~= "bearer" then |
| 53 return false, nil; | 83 return false, nil; |
| 84 end | |
| 85 if not validation_endpoint then | |
| 86 -- We're not going to get more info, only the username | |
| 87 self.username = jid.escape(username); | |
| 88 self.token_info = token_resp; | |
| 89 return true, true; | |
| 54 end | 90 end |
| 55 local ret, err = async.wait_for(self.profile.http_client:request(validation_endpoint, | 91 local ret, err = async.wait_for(self.profile.http_client:request(validation_endpoint, |
| 56 { headers = { ["Authorization"] = "Bearer " .. token_resp.access_token; ["Accept"] = "application/json" } })); | 92 { headers = { ["Authorization"] = "Bearer " .. token_resp.access_token; ["Accept"] = "application/json" } })); |
| 57 if err then | 93 if err then |
| 58 return false, nil; | 94 return false, nil; |
| 59 end | 95 end |
| 60 if not (ret.code >= 200 and ret.code < 300) then | 96 if not (ret.code >= 200 and ret.code < 300) then |
| 61 return false, nil; | 97 return false, nil; |
| 62 end | 98 end |
| 63 local response = json.decode(ret.body); | 99 local response = json.decode(ret.body); |
| 64 if type(response) ~= "table" or (response[username_field]) ~= username then | 100 if type(response) ~= "table" then |
| 101 return false, nil, nil; | |
| 102 elseif type(response[username_field]) ~= "string" then | |
| 65 return false, nil, nil; | 103 return false, nil, nil; |
| 66 end | 104 end |
| 67 if response.jid then | 105 self.username = jid.escape(response[username_field]); |
| 68 self.username, self.realm, self.resource = jid.prepped_split(response.jid, true); | |
| 69 end | |
| 70 self.role = response.role; | |
| 71 self.token_info = response; | 106 self.token_info = response; |
| 72 return true, true; | 107 return true, true; |
| 73 end | 108 end |
| 74 end | 109 end |
| 75 function profile:oauthbearer(token) | 110 if validation_endpoint then |
| 76 if token == "" then | 111 function profile:oauthbearer(token) |
| 77 return false, nil, extra; | 112 if token == "" then |
| 113 return false, nil, extra; | |
| 114 end | |
| 115 | |
| 116 local ret, err = async.wait_for(self.profile.http_client:request(validation_endpoint, { | |
| 117 headers = { ["Authorization"] = "Bearer " .. token; ["Accept"] = "application/json" }; | |
| 118 })); | |
| 119 if err then | |
| 120 return false, nil, extra; | |
| 121 end | |
| 122 local response = ret and json.decode(ret.body); | |
| 123 if not (ret.code >= 200 and ret.code < 300) then | |
| 124 return false, nil, response or extra; | |
| 125 end | |
| 126 if type(response) ~= "table" or type(response[username_field]) ~= "string" then | |
| 127 return false, nil, nil; | |
| 128 end | |
| 129 | |
| 130 return jid.escape(response[username_field]), true, response; | |
| 78 end | 131 end |
| 79 | |
| 80 local ret, err = async.wait_for(self.profile.http_client:request(validation_endpoint, | |
| 81 { headers = { ["Authorization"] = "Bearer " .. token; ["Accept"] = "application/json" } })); | |
| 82 if err then | |
| 83 return false, nil, extra; | |
| 84 end | |
| 85 local response = ret and json.decode(ret.body); | |
| 86 if not (ret.code >= 200 and ret.code < 300) then | |
| 87 return false, nil, response or extra; | |
| 88 end | |
| 89 if type(response) ~= "table" or type(response[username_field]) ~= "string" then | |
| 90 return false, nil, nil; | |
| 91 end | |
| 92 | |
| 93 return response[username_field], true, response; | |
| 94 end | 132 end |
| 95 return sasl.new(host, profile); | 133 return sasl.new(host, profile); |
| 96 end | 134 end |
| 97 | 135 |
| 98 module:provides("auth", provider); | 136 module:provides("auth", provider); |
