-
Notifications
You must be signed in to change notification settings - Fork 81
/
Copy pathauthorization_handlers.cpp
331 lines (303 loc) · 17.2 KB
/
authorization_handlers.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
#include "nmos/authorization_handlers.h"
#include <jwt-cpp/jwt.h>
#include "cpprest/basic_utils.h"
#include "cpprest/json_validator.h"
#include "cpprest/response_type.h"
#include "nmos/api_utils.h" // for nmos::experimental::details::make_validate_authorization_handler
#include "nmos/authorization_state.h"
#include "nmos/is10_versions.h"
#include "nmos/json_schema.h"
#include "nmos/json_fields.h"
#include "nmos/slog.h"
#if defined(_WIN32) && !defined(__cplusplus_winrt)
#include <Windows.h>
#include <shellapi.h>
#endif
namespace nmos
{
namespace experimental
{
namespace details
{
static const web::json::experimental::json_validator& auth_clients_schema_validator()
{
static const web::json::experimental::json_validator validator
{
nmos::experimental::load_json_schema,
boost::copy_range<std::vector<web::uri>>(is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_register_client_response_uri))
};
return validator;
}
}
// helper function to load the authorization clients file
// example of the file
// [
// {
// "authorization_server_uri": "https://example.com"
// },
// {
// "client_metadata": {
// "client_id": "acc8fd35-327d-4486-a02f-9a8fdc25a609",
// "client_name" : "example client",
// "grant_types" : [ "authorization_code", "client_credentials","refresh_token" ],
// "jwks_uri" : "https://example_client/jwks",
// "redirect_uris" : [ "https://example_client/callback" ],
// "registration_access_token" : "eyJhbGci....",
// "registration_client_uri" : "https://example.com/openid-connect/acc8fd35-327d-4486-a02f-9a8fdc25a609",
// "response_types" : [ "code" ],
// "scope" : "registration",
// "subject_type" : "public",
// "tls_client_certificate_bound_access_tokens" : false,
// "token_endpoint_auth_method" : "private_key_jwt"
// }
// }
// ]
web::json::value load_authorization_clients_file(const utility::string_t& filename, slog::base_gate& gate)
{
using web::json::value;
try
{
utility::ifstream_t is(filename);
if (is.is_open())
{
const auto authorization_clients = value::parse(is);
if (!authorization_clients.is_null() && authorization_clients.is_array() && authorization_clients.as_array().size())
{
for (auto& authorization_client : authorization_clients.as_array())
{
if (authorization_client.has_field(nmos::experimental::fields::authorization_server_uri) &&
!authorization_client.at(nmos::experimental::fields::authorization_server_uri).as_string().empty() &&
authorization_client.has_field(nmos::experimental::fields::client_metadata))
{
// validate client metadata
const auto& client_metadata = authorization_client.at(nmos::experimental::fields::client_metadata);
details::auth_clients_schema_validator().validate(client_metadata, experimental::make_authapi_register_client_response_uri(is10_versions::v1_0)); // may throw json_exception
}
}
}
return authorization_clients;
}
}
catch (const web::json::json_exception& e)
{
slog::log<slog::severities::warning>(gate, SLOG_FLF) << "Unable to load authorization clients from non-volatile memory: " << filename << ": " << e.what();
}
return web::json::value::array();
}
// helper function to update the authorization clients file
// example of authorization_client
// {
// {
// "authorization_server_uri": "https://example.com"
// },
// {
// "client_metadata": {
// "client_id": "acc8fd35-327d-4486-a02f-9a8fdc25a609",
// "client_name" : "example client",
// "grant_types" : [ "authorization_code", "client_credentials","refresh_token" ],
// "issuer" : "https://example.com",
// "jwks_uri" : "https://example_client/jwks",
// "redirect_uris" : [ "https://example_client/callback" ],
// "registration_access_token" : "eyJhbGci....",
// "registration_client_uri" : "https://example.com/openid-connect/acc8fd35-327d-4486-a02f-9a8fdc25a609",
// "response_types" : [ "code" ],
// "scope" : "registration",
// "subject_type" : "public",
// "tls_client_certificate_bound_access_tokens" : false,
// "token_endpoint_auth_method" : "private_key_jwt"
// }
// }
// }
void update_authorization_clients_file(const utility::string_t& filename, const web::json::value& authorization_client, slog::base_gate& gate)
{
// load authorization_clients from file
auto authorization_clients = load_authorization_clients_file(filename, gate);
// update/append to the authorization_clients
bool updated{ false };
if (authorization_clients.as_array().size())
{
for (auto& auth_client : authorization_clients.as_array())
{
const auto& authorization_server_uri = auth_client.at(nmos::experimental::fields::authorization_server_uri);
if (authorization_server_uri == authorization_client.at(nmos::experimental::fields::authorization_server_uri))
{
auth_client[nmos::experimental::fields::client_metadata] = authorization_client.at(nmos::experimental::fields::client_metadata);
updated = true;
break;
}
}
}
if (!updated)
{
web::json::push_back(authorization_clients, authorization_client);
}
// save the updated authorization_clients to file
utility::ofstream_t os(filename, std::ios::out | std::ios::trunc);
if (os.is_open())
{
os << authorization_clients.serialize();
os.close();
}
}
// construct callback to load a table of authorization server uri vs authorization client metadata from file based on settings seed_id
// it is not required for scopeless OAuth 2.0 client (client not require to access any protected APIs)
load_authorization_clients_handler make_load_authorization_clients_handler(const nmos::settings& settings, slog::base_gate& gate)
{
return [&]()
{
// obtain client metadata from the safe, permission-restricted, location in the non-volatile memory, e.g. a file
// Client metadata SHOULD consist of the client_id, client_secret, client_secret_expires_at, client_uri, grant_types, redirect_uris, response_types, scope, token_endpoint_auth_method
auto filename = nmos::experimental::fields::seed_id(settings) + U(".json");
slog::log<slog::severities::more_info>(gate, SLOG_FLF) << "Load authorization clients from non-volatile memory: " << filename;
return load_authorization_clients_file(filename, gate);
};
}
// construct callback to save the authorization server uri vs authorization client metadata table to file, using seed_id for the filename
// it is not required for scopeless OAuth 2.0 client (client not require to access any protected APIs)
save_authorization_client_handler make_save_authorization_client_handler(const nmos::settings& settings, slog::base_gate& gate)
{
return [&](const web::json::value& authorization_client)
{
// Client metadata SHOULD be stored in a safe, permission-restricted, location in non-volatile memory in case of a device restart to prevent re-registration.
// Client secrets SHOULD be encrypted before being stored to reduce the chance of client secret leaking.
// see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#client-credentials
const auto filename = nmos::experimental::fields::seed_id(settings) + U(".json");
slog::log<slog::severities::more_info>(gate, SLOG_FLF) << "Save authorization clients to non-volatile memory: " << filename;
update_authorization_clients_file(filename, authorization_client, gate);
};
}
// construct callback to start the authorization code flow request on a browser
// this is required for OAuth clients which use Authorization Code Flow to obtain the access token
// note: as it is not easy to specify the 'content-type' used in the browser programmatically, this can be easily
// fixed by installing a browser header modifier
// extensions such as ModHeader can be used to add the missing 'content-type' header:
// for Windows https://chrome.google.com/webstore/detail/modheader-modify-http-hea/idgpnmonknjnojddfkpgkljpfnnfcklj
// for Linux https://addons.mozilla.org/en-GB/firefox/addon/modheader-firefox/
request_authorization_code_handler make_request_authorization_code_handler(slog::base_gate& gate)
{
return[&gate](const web::uri& authorization_code_uri)
{
slog::log<slog::severities::more_info>(gate, SLOG_FLF) << "Open a browser to start the authorization code flow: " << authorization_code_uri.to_string();
std::string browser_cmd;
#if defined(_WIN32) && !defined(__cplusplus_winrt)
browser_cmd = "start \"\" \"" + utility::us2s(authorization_code_uri.to_string()) + "\"";
#else
browser_cmd = "xdg-open \"" + utility::us2s(authorization_code_uri.to_string()) + "\"";
#endif
std::ignore = system(browser_cmd.c_str());
// hmm, notify authorization_code_flow in the authorization_behaviour thread
// in the event of user cancels the authorization code flow process
};
}
// construct callback to validate OAuth 2.0 authorization access token
validate_authorization_token_handler make_validate_authorization_token_handler(authorization_state& authorization_state, slog::base_gate& gate)
{
return[&](const utility::string_t& access_token)
{
try
{
// extract the token issuer from the token
const auto token_issuer = nmos::experimental::jwt_validator::get_token_issuer(access_token);
auto lock = authorization_state.read_lock();
std::string error;
auto issuer = authorization_state.issuers.find(token_issuer);
if (authorization_state.issuers.end() != issuer)
{
slog::log<slog::severities::more_info>(gate, SLOG_FLF) << "Validate access token against " << utility::us2s(issuer->first.to_string()) << " public keys";
try
{
// if jwt_validator is not already set up, assume no public keys to validate token
if (issuer->second.jwt_validator.is_initialized())
{
// do access token basic validation, including token schema validation and token issuer public keys validation
issuer->second.jwt_validator.basic_validation(access_token);
return authorization_error{ authorization_error::succeeded };
}
else
{
std::stringstream ss;
ss << "No " << utility::us2s(issuer->first.to_string()) << " public keys to validate access token";
error = ss.str();
slog::log<slog::severities::error>(gate, SLOG_FLF) << error;
return authorization_error{ authorization_error::no_matching_keys, error };
}
}
catch (const web::json::json_exception& e)
{
#if defined (NDEBUG)
slog::log<slog::severities::error>(gate, SLOG_FLF) << "JSON error: " << e.what();
#else
slog::log<slog::severities::error>(gate, SLOG_FLF) << "JSON error: " << e.what() << "; access_token: " << access_token;
#endif
return authorization_error{ authorization_error::failed, e.what() };
}
catch (const jwt::error::token_verification_exception& e)
{
#if defined (NDEBUG)
slog::log<slog::severities::error>(gate, SLOG_FLF) << "Token verification error: " << e.what();
#else
slog::log<slog::severities::error>(gate, SLOG_FLF) << "Token verification error: " << e.what() << "; access_token: " << access_token;
#endif
return authorization_error{ authorization_error::failed, e.what() };
}
catch (const no_matching_keys_exception& e)
{
#if defined (NDEBUG)
slog::log<slog::severities::error>(gate, SLOG_FLF) << "No matching public keys error: " << e.what();
#else
slog::log<slog::severities::error>(gate, SLOG_FLF) << "No matching public keys error: " << e.what() << "; access_token: " << access_token;
#endif
return authorization_error{ authorization_error::no_matching_keys, e.what() };
}
catch (const std::exception& e)
{
#if defined (NDEBUG)
slog::log<slog::severities::error>(gate, SLOG_FLF) << "Unexpected exception: " << e.what();
#else
slog::log<slog::severities::error>(gate, SLOG_FLF) << "Unexpected exception: " << e.what() << "; access_token: " << access_token;
#endif
return authorization_error{ authorization_error::failed, e.what() };
}
}
else
{
std::stringstream ss;
ss << "No " << utility::us2s(token_issuer.to_string()) << " public keys to validate access token";
error = ss.str();
slog::log<slog::severities::error>(gate, SLOG_FLF) << error;
// no public keys to validate token
return authorization_error{ authorization_error::no_matching_keys, error };
}
}
catch (const std::exception& e)
{
#if defined (NDEBUG)
slog::log<slog::severities::error>(gate, SLOG_FLF) << "Unable to extract token issuer from access token: " << e.what();
#else
slog::log<slog::severities::error>(gate, SLOG_FLF) << "Unable to extract token issuer from access token: " << e.what() << "; access_token: " << access_token;
#endif
return authorization_error{ authorization_error::failed, e.what() };
}
};
}
// construct callback to validate OAuth 2.0 authorization
validate_authorization_handler make_validate_authorization_handler(nmos::base_model& model, authorization_state& authorization_state, validate_authorization_token_handler access_token_validation, slog::base_gate& gate)
{
return[&, access_token_validation](const nmos::experimental::scope& scope)
{
slog::log<slog::severities::more_info>(gate, SLOG_FLF) << "Make authorization validation";
return nmos::experimental::details::make_validate_authorization_handler(model, authorization_state, scope, access_token_validation, gate);
};
}
// construct callback to retrieve OAuth 2.0 authorization bearer token
get_authorization_bearer_token_handler make_get_authorization_bearer_token_handler(authorization_state& authorization_state, slog::base_gate& gate)
{
return[&]()
{
slog::log<slog::severities::more_info>(gate, SLOG_FLF) << "Retrieve bearer token from cache";
auto lock = authorization_state.read_lock();
return authorization_state.bearer_token;
};
}
}
}