Mercurial > prosody-modules
comparison mod_http_oauth2/mod_http_oauth2.lua @ 4256:c4b9d4ba839b
mod_http_oauth2: Authorization code flow
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Sat, 21 Nov 2020 01:08:30 +0100 |
parents | a0ab7be0538d |
children | 145e8e8a247a |
comparison
equal
deleted
inserted
replaced
4255:38da10e4b593 | 4256:c4b9d4ba839b |
---|---|
1 local http = require "util.http"; | 1 local http = require "util.http"; |
2 local jid = require "util.jid"; | 2 local jid = require "util.jid"; |
3 local json = require "util.json"; | 3 local json = require "util.json"; |
4 local usermanager = require "core.usermanager"; | 4 local usermanager = require "core.usermanager"; |
5 local errors = require "util.error"; | 5 local errors = require "util.error"; |
6 local url = require "socket.url"; | |
7 local uuid = require "util.uuid"; | |
8 local encodings = require "util.encodings"; | |
9 local base64 = encodings.base64; | |
6 | 10 |
7 local tokens = module:depends("tokenauth"); | 11 local tokens = module:depends("tokenauth"); |
12 | |
13 local clients = module:open_store("oauth2_clients"); | |
14 local codes = module:open_store("oauth2_codes", "map"); | |
8 | 15 |
9 local function oauth_error(err_name, err_desc) | 16 local function oauth_error(err_name, err_desc) |
10 return errors.new({ | 17 return errors.new({ |
11 type = "modify"; | 18 type = "modify"; |
12 condition = "bad-request"; | 19 condition = "bad-request"; |
25 -- TODO: include refresh_token when implemented | 32 -- TODO: include refresh_token when implemented |
26 }; | 33 }; |
27 end | 34 end |
28 | 35 |
29 local grant_type_handlers = {}; | 36 local grant_type_handlers = {}; |
37 local response_type_handlers = {}; | |
30 | 38 |
31 function grant_type_handlers.password(params) | 39 function grant_type_handlers.password(params) |
32 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); | 40 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); |
33 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); | 41 local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); |
34 local request_username, request_host, request_resource = jid.prepped_split(request_jid); | 42 local request_username, request_host, request_resource = jid.prepped_split(request_jid); |
43 return json.encode(new_access_token(granted_jid, request_host, nil, nil)); | 51 return json.encode(new_access_token(granted_jid, request_host, nil, nil)); |
44 end | 52 end |
45 return oauth_error("invalid_grant", "incorrect credentials"); | 53 return oauth_error("invalid_grant", "incorrect credentials"); |
46 end | 54 end |
47 | 55 |
56 function response_type_handlers.code(params, granted_jid) | |
57 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end | |
58 if not params.redirect_uri then return oauth_error("invalid_request", "missing 'redirect_uri'"); end | |
59 if params.scope and params.scope ~= "" then | |
60 return oauth_error("invalid_scope", "unknown scope requested"); | |
61 end | |
62 | |
63 local client, err = clients:get(params.client_id); | |
64 module:log("debug", "clients:get(%q) --> %q, %q", params.client_id, client, err); | |
65 if err then error(err); end | |
66 if not client then | |
67 return oauth_error("invalid_client", "incorrect credentials"); | |
68 end | |
69 | |
70 local code = uuid.generate(); | |
71 assert(codes:set(params.client_id, code, { issued = os.time(), granted_jid = granted_jid, })); | |
72 | |
73 local redirect = url.parse(params.redirect_uri); | |
74 local query = http.formdecode(redirect.query or ""); | |
75 if type(query) ~= "table" then query = {}; end | |
76 table.insert(query, { name = "code", value = code }) | |
77 if params.state then | |
78 table.insert(query, { name = "state", value = params.state }); | |
79 end | |
80 redirect.query = http.formencode(query); | |
81 | |
82 return { | |
83 status_code = 302; | |
84 headers = { | |
85 location = url.build(redirect); | |
86 }; | |
87 } | |
88 end | |
89 | |
90 function grant_type_handlers.authorization_code(params) | |
91 if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end | |
92 if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end | |
93 if not params.code then return oauth_error("invalid_request", "missing 'code'"); end | |
94 --if not params.redirect_uri then return oauth_error("invalid_request", "missing 'redirect_uri'"); end | |
95 if params.scope and params.scope ~= "" then | |
96 return oauth_error("invalid_scope", "unknown scope requested"); | |
97 end | |
98 | |
99 local client, err = clients:get(params.client_id); | |
100 if err then error(err); end | |
101 if not client or client.secret ~= params.client_secret then | |
102 return oauth_error("invalid_client", "incorrect credentials"); | |
103 end | |
104 local code, err = codes:get(params.client_id, params.code); | |
105 if err then error(err); end | |
106 if not code or type(code) ~= "table" or os.difftime(os.time(), code.issued) > 900 then | |
107 return oauth_error("invalid_client", "incorrect credentials"); | |
108 end | |
109 assert(codes:set(params.client_id, params.code, nil)); | |
110 | |
111 if client.redirect_uri and client.redirect_uri ~= params.redirect_uri then | |
112 return oauth_error("invalid_client", "incorrect 'redirect_uri'"); | |
113 end | |
114 | |
115 return json.encode(new_access_token(code.granted_jid, nil, nil)); | |
116 end | |
117 | |
118 local function check_credentials(request) | |
119 local auth_type, auth_data = string.match(request.headers.authorization, "^(%S+)%s(.+)$"); | |
120 | |
121 if auth_type == "Basic" then | |
122 local creds = base64.decode(auth_data); | |
123 if not creds then return false; end | |
124 local username, password = string.match(creds, "^([^:]+):(.*)$"); | |
125 if not username then return false; end | |
126 username, password = encodings.stringprep.nodeprep(username), encodings.stringprep.saslprep(password); | |
127 if not username then return false; end | |
128 if not usermanager.test_password(username, module.host, password) then | |
129 return false; | |
130 end | |
131 return username; | |
132 end | |
133 return nil; | |
134 end | |
135 | |
48 if module:get_host_type() == "component" then | 136 if module:get_host_type() == "component" then |
49 local component_secret = assert(module:get_option_string("component_secret"), "'component_secret' is a required setting when loaded on a Component"); | 137 local component_secret = assert(module:get_option_string("component_secret"), "'component_secret' is a required setting when loaded on a Component"); |
50 | 138 |
51 function grant_type_handlers.password(params) | 139 function grant_type_handlers.password(params) |
52 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); | 140 local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); |
62 local granted_jid = jid.join(request_username, request_host, request_resource); | 150 local granted_jid = jid.join(request_username, request_host, request_resource); |
63 return json.encode(new_access_token(granted_jid, request_host, nil, nil)); | 151 return json.encode(new_access_token(granted_jid, request_host, nil, nil)); |
64 end | 152 end |
65 return oauth_error("invalid_grant", "incorrect credentials"); | 153 return oauth_error("invalid_grant", "incorrect credentials"); |
66 end | 154 end |
155 | |
156 -- TODO How would this make sense with components? | |
157 -- Have an admin authenticate maybe? | |
158 response_type_handlers.code = nil; | |
159 grant_type_handlers.authorization_code = nil; | |
160 check_credentials = function () return false end | |
67 end | 161 end |
68 | 162 |
69 function handle_token_grant(event) | 163 function handle_token_grant(event) |
70 event.response.headers.content_type = "application/json"; | 164 event.response.headers.content_type = "application/json"; |
71 local params = http.formdecode(event.request.body); | 165 local params = http.formdecode(event.request.body); |
78 return oauth_error("unsupported_grant_type"); | 172 return oauth_error("unsupported_grant_type"); |
79 end | 173 end |
80 return grant_handler(params); | 174 return grant_handler(params); |
81 end | 175 end |
82 | 176 |
177 local function handle_authorization_request(event) | |
178 if not event.request.headers.authorization then | |
179 event.response.headers.www_authenticate = string.format("Basic realm=%q", module.host.."/"..module.name); | |
180 return 401; | |
181 end | |
182 local user = check_credentials(event.request); | |
183 if not user then | |
184 return 401; | |
185 end | |
186 if not event.request.url.query then | |
187 event.response.headers.content_type = "application/json"; | |
188 return oauth_error("invalid_request"); | |
189 end | |
190 local params = http.formdecode(event.request.url.query); | |
191 if not params then | |
192 return oauth_error("invalid_request"); | |
193 end | |
194 local response_type = params.response_type; | |
195 local response_handler = response_type_handlers[response_type]; | |
196 if not response_handler then | |
197 event.response.headers.content_type = "application/json"; | |
198 return oauth_error("unsupported_response_type"); | |
199 end | |
200 return response_handler(params, jid.join(user, module.host)); | |
201 end | |
202 | |
83 module:depends("http"); | 203 module:depends("http"); |
84 module:provides("http", { | 204 module:provides("http", { |
85 route = { | 205 route = { |
86 ["POST /token"] = handle_token_grant; | 206 ["POST /token"] = handle_token_grant; |
207 ["GET /authorize"] = handle_authorization_request; | |
87 }; | 208 }; |
88 }); | 209 }); |
89 | 210 |
90 local http_server = require "net.http.server"; | 211 local http_server = require "net.http.server"; |
91 | 212 |