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