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