Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
OAuthDeviceAuthorizationResource.cs
1using System;
3using System.Diagnostics.CodeAnalysis;
4using System.Text;
5using System.Threading.Tasks;
6using Waher.Content;
13using Waher.Script;
14using Waher.Security;
17
19{
26 {
30 public const string DefaultResourcePath = "/oauth/device";
31
35 public const string GrantType = "urn:ietf:params:oauth:grant-type:device_code";
36
41 public const int MinimumIntervalSeconds = 5;
42
43 private static readonly Cache<string, DeviceRef> codes = new Cache<string, DeviceRef>(int.MaxValue, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
44
51 {
52 }
53
60 string ResourceName)
62 {
64 }
65
69 public bool AllowsGET => true;
70
74 public bool AllowsPOST => true;
75
82 public async Task GET(HttpRequest Request, HttpResponse Response)
83 {
84 if (!Request.Header.TryGetQueryParameter("user_code", out string UserCode))
85 UserCode = string.Empty;
86
87 if (!Request.Header.TryGetQueryParameter("client_id", out string ClientId))
88 ClientId = string.Empty;
89
90 string ErrorMessage = string.Empty;
91 bool AlreadyResponded = false;
92
93 if (!string.IsNullOrEmpty(UserCode))
94 {
95 if (!codes.TryGetValue(UserCode, out DeviceRef DeviceRef))
96 ErrorMessage = "Invalid User Code";
97 else if (DeviceRef.Result.HasValue)
98 {
99 AlreadyResponded = true;
100
101 if (DeviceRef.Result.Value)
102 ErrorMessage = "Authorization request has already been accepted.";
103 else
104 ErrorMessage = "Authorization request has already been declined.";
105 }
106 }
107
108 await Response.Return(await this.GenerateAuthorizationForm(Request, Response,
109 ClientId, UserCode, string.Empty, false, false, AlreadyResponded,
110 ErrorMessage));
111 }
112
119 public async Task POST(HttpRequest Request, HttpResponse Response)
120 {
121 if (!Request.HasData)
122 {
123 await BadRequest(Response, "invalid_request", "No payload in request.");
124 return;
125 }
126
127 ContentResponse Content = await Request.DecodeDataAsync();
128 if (Content.HasError || !(Content.Decoded is Dictionary<string, string> Form))
129 {
130 await BadRequest(Response, "invalid_request",
131 "Expected URL-encoded WWW form.");
132 return;
133 }
134
135 if (!(this.Users is IThingRegistryUserSource ThingRegistry))
136 {
137 await ServiceUnavailable(Response, "server_error",
138 "Device authorization service not available.");
139 return;
140 }
141
142 if (Form.TryGetValue("user_code", out string UserCode))
143 {
144 if (!Form.TryGetValue("p", out string ParametersToken) ||
145 string.IsNullOrEmpty(ParametersToken) ||
146 !JwtToken.TryParse(ParametersToken, out JwtToken? Parameters) ||
147 !this.JwtFactory.IsValid(Parameters) ||
148 !Parameters.TryGetClaim("client_id", out object Obj) ||
149 !(Obj is string ClientId))
150 {
151 await BadRequest(Response, "invalid_request", "Missing or invalid parameters provided.");
152 return;
153 }
154
155 if (!Form.TryGetValue("UserName", out string UserName))
156 UserName = string.Empty;
157
158 if (!Form.TryGetValue("Password", out string Password))
159 Password = string.Empty;
160
161 if (!Form.TryGetValue("Accept", out string s) ||
162 !CommonTypes.TryParse(s, out bool Accept))
163 {
164 Accept = false;
165 }
166
167 if (!Form.TryGetValue("Decline", out s) ||
168 !CommonTypes.TryParse(s, out bool Decline))
169 {
170 Decline = false;
171 }
172
173 IUserWithClaims? Device;
174 IUser? Owner;
175 bool AlreadyResponded = false;
176
177 if (string.IsNullOrEmpty(UserCode))
178 s = "Missing User Code";
179 else if (!codes.TryGetValue(UserCode, out DeviceRef DeviceRef) ||
180 DeviceRef.UserCode != UserCode) // If attempting to use the device_code
181 {
182 s = "Invalid User Code";
183 }
184 else if (string.IsNullOrEmpty(UserName))
185 s = "Missing user name.";
186 else if (string.IsNullOrEmpty(Password))
187 s = "Missing password.";
188 else if (!Accept && !Decline)
189 s = "You must either accept or decline the authorization request.";
190 else if (Accept && Decline)
191 s = "You cannot both accept and decline the authorization request.";
192 else if ((Device = await this.Users.TryGetUser(DeviceRef.Device.UserName) as IUserWithClaims) is null)
193 s = "Device no longer registered.";
194 else if ((Owner = await ThingRegistry.TryGetOwner(Device)) is null)
195 s = "Device no longer has owner registered.";
196 else if (Owner.UserName != UserName ||
197 UserCode != ComputeUserCode(DeviceRef.DeviceCode, DeviceRef.Device.UserName,
198 UserName, BasicAuthentication.ComputePasswordHash(UserName,
199 this.Environment.Realm, Password, Owner.PasswordHashType)))
200 {
201 s = "Invalid user name, password, or owner.";
202 }
203 else if (DeviceRef.Result.HasValue)
204 {
205 AlreadyResponded = true;
206
207 if (DeviceRef.Result.Value)
208 s = "Authorization request has already been accepted.";
209 else
210 s = "Authorization request has already been declined.";
211 }
212 else
213 {
214 DeviceRef.Result = Accept;
215 await Response.Return(await this.GenerateResult(Request, Response, Accept));
216 return;
217 }
218
219 await Response.Return(await this.GenerateAuthorizationForm(Request, Response,
220 ClientId, UserCode, UserName, Accept, Decline, AlreadyResponded, s));
221 }
222 else if (Form.TryGetValue("client_id", out string ClientId))
223 {
224 if (string.IsNullOrEmpty(ClientId))
225 {
226 await BadRequest(Response, "invalid_request", "Empty client_id.");
227 return;
228 }
229
230 IUser? Device = await ThingRegistry.TryGetUser(ClientId);
231 if (Device is null)
232 {
233 await ServiceUnavailable(Response, "access_denied", "Device or owner not registered.");
234 return;
235 }
236
237 if (!(Device is IUserWithClaims DeviceClaims))
238 {
239 await ServiceUnavailable(Response, "access_denied", "Device cannot be used in this interface.");
240 return;
241 }
242
243 IUser? Owner = await ThingRegistry.TryGetOwner(Device);
244 if (Owner is null)
245 {
246 await ServiceUnavailable(Response, "access_denied", "Device or owner not registered.");
247 return;
248 }
249
250 if (!Form.TryGetValue("scope", out string Scope))
251 Scope = string.Empty;
252 else if (!IsValidScope(Scope))
253 {
254 await BadRequest(Response, "invalid_scope", "Invalid scope parameter.");
255 return;
256 }
257
258 StringBuilder sb;
259 string DeviceCode;
260
261 do
262 {
263 DeviceCode = OAuth2Environment.GenerateRandomCode(32);
264 UserCode = ComputeUserCode(DeviceCode, ClientId, Owner.UserName, Owner.PasswordHash);
265 }
266 while (
267 codes.ContainsKey(UserCode) ||
268 codes.ContainsKey(DeviceCode));
269
270 DeviceRef Ref = new DeviceRef(DeviceClaims, Owner, Scope, DeviceCode, UserCode);
271 codes.Add(UserCode, Ref);
272 codes.Add(DeviceCode, Ref);
273
274 Response.SetHeader("Cache-Control", "max-age=0, no-cache, no-store");
275 Response.SetHeader("Pragma", "no-cache");
276
278 sb.Append(this.ResourceName);
279 string VerificationUrl = sb.ToString();
280
281 sb.Append("?user_code=");
282 sb.Append(UserCode);
283
284 sb.Append("&client_id=");
285 sb.Append(ClientId);
286
287 string VerificationUrlComplete = sb.ToString();
288
289 await Response.Return(new Dictionary<string, object>()
290 {
291 { "device_code", DeviceCode },
292 { "user_code", UserCode },
293 { "verification_uri", VerificationUrl },
294 { "verification_uri_complete", VerificationUrlComplete },
295 { "expires_in", 3600 },
296 { "interval", MinimumIntervalSeconds }
297 });
298 }
299 else
300 await BadRequest(Response, "invalid_request", "Missing or invalid parameters provided.");
301 }
302
303 private static string ComputeUserCode(string DeviceCode, string ClientId,
304 string OwnerUserName, string OwnerPasswordHash)
305 {
306 StringBuilder sb = new StringBuilder();
307 sb.Append(DeviceCode);
308 sb.Append('|');
309 sb.Append(ClientId);
310 sb.Append('|');
311 sb.Append(OwnerUserName);
312 sb.Append('|');
313 sb.Append(OwnerPasswordHash);
314
315 SHAKE256 H = new SHAKE256(64);
316 return Base64Url.Encode(H.ComputeVariable(Encoding.UTF8.GetBytes(sb.ToString())));
317 }
318
319 internal bool TryGetDeviceReference(string DeviceCode,
320 [NotNullWhen(true)] out DeviceRef? Reference)
321 {
322 if (!codes.TryGetValue(DeviceCode, out Reference))
323 return false;
324
325 if (Reference.DeviceCode != DeviceCode) // if attempting to use the user_code
326 {
327 Reference = null;
328 return false;
329 }
330
331 return true;
332 }
333
334 internal class DeviceRef
335 {
336 public DeviceRef(IUserWithClaims Device, IUser Owner, string Scope,
337 string DeviceCode, string UserCode)
338 {
339 this.Device = Device;
340 this.Owner = Owner;
341 this.Scope = Scope;
342 this.DeviceCode = DeviceCode;
343 this.UserCode = UserCode;
344 }
345
346 public void Remove()
347 {
348 codes.Remove(this.DeviceCode);
349 codes.Remove(this.UserCode);
350 }
351
352 public IUserWithClaims Device;
353 public DateTime? LastPoll = null;
354 public IUser Owner;
355 public string Scope;
356 public string DeviceCode;
357 public string UserCode;
358 public bool? Result;
359 }
360
361 private async Task<HtmlDocument> GenerateAuthorizationForm(HttpRequest Request,
362 HttpResponse Response, string ClientId, string UserCode, string UserName,
363 bool Accept, bool Decline, bool AlreadyResponded, string ErrorMessage)
364 {
365 StringBuilder Markdown = new StringBuilder();
366
367 Markdown.AppendLine("Title: Device Authorization");
368 Markdown.AppendLine("Description: OAUTH device authorization page.");
369
371 {
372 Markdown.Append("Master: ");
373 Markdown.AppendLine(this.Environment.LoginMasterFileName);
374 }
375
376 Markdown.Append("Date: ");
377 Markdown.AppendLine(CommonTypes.EncodeRfc822(DateTime.UtcNow));
378 Markdown.AppendLine();
379 Markdown.AppendLine(new string('=', 40));
380 Markdown.AppendLine();
381
382 Markdown.AppendLine("Device Authorization");
383 Markdown.AppendLine("=======================");
384 Markdown.AppendLine();
385 Markdown.Append("A Device");
386
387 if (!string.IsNullOrEmpty(ClientId))
388 {
389 Markdown.Append(" with ID `");
390 Markdown.Append(ClientId);
391 Markdown.Append("`");
392 }
393
394 Markdown.Append(" is requesting authorization to connect. As its registered ");
395 Markdown.Append("owner, you can either accept or decline this request, by ");
396 Markdown.Append("providing your credentials below, selecting the appropriate ");
397 Markdown.AppendLine("option, and submit the form.");
398 Markdown.AppendLine();
399
400 Markdown.Append("<form id='AuthorizationForm' action='");
401 Markdown.Append(this.ResourceName);
402 Markdown.Append("' method='post'>");
403 Markdown.AppendLine();
404
405 if (!AlreadyResponded)
406 {
407 string ParametersToken = this.JwtFactory.Create(
408 new KeyValuePair<string, object>("client_id", ClientId));
409
410 Markdown.Append("<input type='hidden' name='p' value='");
411 Markdown.Append(XML.HtmlAttributeEncode(ParametersToken));
412 Markdown.AppendLine("'/>");
413 Markdown.AppendLine();
414 Markdown.AppendLine("<p>");
415 Markdown.AppendLine("<label for='user_code'>User Code:</label> ");
416 Markdown.Append("<input id='user_code' name='user_code' type='text' autofocus autocomplete='off");
417
418 if (!string.IsNullOrEmpty(UserCode))
419 {
420 Markdown.Append("' value='");
421 Markdown.Append(XML.HtmlAttributeEncode(UserCode));
422 }
423
424 Markdown.AppendLine("'/>");
425 Markdown.AppendLine("</p>");
426 Markdown.AppendLine();
427
428 Markdown.AppendLine("<p>");
429 Markdown.Append("<input id='Accept' name='Accept' type='checkbox' ");
430 Markdown.Append("title='Check this box to authorize the device access.'");
431 if (Accept)
432 Markdown.Append(" checked");
433 Markdown.AppendLine("/>");
434 Markdown.AppendLine("<label for='Accept'>Accept authorization.</label> ");
435 Markdown.AppendLine("</p>");
436 Markdown.AppendLine();
437
438 Markdown.AppendLine("<p>");
439 Markdown.Append("<input id='Decline' name='Decline' type='checkbox' ");
440 Markdown.Append("title='Check this box to decline the authorization request.'");
441 if (Decline)
442 Markdown.Append(" checked");
443 Markdown.AppendLine("/>");
444 Markdown.AppendLine("<label for='Decline'>Decline authorization.</label> ");
445 Markdown.AppendLine("</p>");
446 Markdown.AppendLine();
447
448 Markdown.AppendLine("<p>");
449 Markdown.AppendLine("<label for='UserName'>User Name:</label> ");
450 Markdown.Append("<input id='UserName' name='UserName' type='text' autocomplete='username");
451 if (!string.IsNullOrEmpty(UserName))
452 {
453 Markdown.Append("' value='");
454 Markdown.Append(XML.HtmlAttributeEncode(UserName));
455 }
456 Markdown.AppendLine("'/>");
457 Markdown.AppendLine("</p>");
458 Markdown.AppendLine();
459
460 Markdown.AppendLine("<p>");
461 Markdown.AppendLine("<label for='Password'>Password:</label> ");
462 Markdown.Append("<input id='Password' name='Password' type='password' ");
463 Markdown.AppendLine("autocomplete='current-password'/>");
464 Markdown.AppendLine("</p>");
465 Markdown.AppendLine();
466 }
467
468 if (!string.IsNullOrEmpty(ErrorMessage))
469 {
470 Markdown.AppendLine("<p>");
471 Markdown.Append("<strong id='errorMessage'>");
472 Markdown.Append(XML.HtmlValueEncode(ErrorMessage));
473 Markdown.AppendLine("</strong>");
474 Markdown.AppendLine("</p>");
475 Markdown.AppendLine();
476 }
477
478 if (!AlreadyResponded)
479 Markdown.AppendLine("<button type='submit'>Submit</button>");
480
481 Markdown.AppendLine("</form>");
482
483 string Markdown2 = await this.Environment.RaiseCustomizeDeviceLoginForm(Markdown.ToString());
484
485 MarkdownDocument Doc = await MarkdownDocument.CreateAsync(Markdown2,
486 new MarkdownSettings()
487 {
488 Variables = new Variables()
489 });
490
491 string Html = await Doc.GenerateHTML();
492
493 Response.SetHeader("X-Frame-Options", "DENY");
494 Response.SetHeader("Content-Security-Policy", "frame-ancestors 'none'; default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'");
495
496 return new HtmlDocument(Html);
497 }
498
499 private async Task<HtmlDocument> GenerateResult(HttpRequest Request,
500 HttpResponse Response, bool Accepted)
501 {
502 StringBuilder Markdown = new StringBuilder();
503
504 Markdown.AppendLine("Title: Accepted");
505 Markdown.AppendLine("Description: OAUTH device authorization has been accepted.");
506
508 {
509 Markdown.Append("Master: ");
510 Markdown.AppendLine(this.Environment.LoginMasterFileName);
511 }
512
513 Markdown.Append("Date: ");
514 Markdown.AppendLine(CommonTypes.EncodeRfc822(DateTime.UtcNow));
515 Markdown.AppendLine();
516 Markdown.AppendLine(new string('=', 40));
517 Markdown.AppendLine();
518
519 if (Accepted)
520 Markdown.AppendLine("Accepted");
521 else
522 Markdown.AppendLine("Declined");
523
524 Markdown.AppendLine("===========");
525 Markdown.AppendLine();
526
527 Markdown.Append("Authorization request has been ");
528
529 if (Accepted)
530 {
531 Markdown.AppendLine("accepted.");
532 Markdown.AppendLine("The device will be informed and granted access.");
533 }
534 else
535 {
536 Markdown.AppendLine("declined.");
537 Markdown.AppendLine("The device will be informed access has been denied.");
538 }
539
540 Markdown.AppendLine("You can safely close this tab.");
541
542 string Markdown2 = await this.Environment.RaiseCustomizeDeviceLoginReceipt(Markdown.ToString());
543
544 MarkdownDocument Doc = await MarkdownDocument.CreateAsync(Markdown2,
545 new MarkdownSettings()
546 {
547 Variables = new Variables()
548 });
549
550 string Html = await Doc.GenerateHTML();
551
552 Response.SetHeader("X-Frame-Options", "DENY");
553 Response.SetHeader("Content-Security-Policy", "frame-ancestors 'none'; default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'");
554
555 return new HtmlDocument(Html);
556 }
557
558 }
559}
Static class that does BASE64URL encoding (using URL and filename safe alphabet), as defined in RFC46...
Definition: Base64Url.cs:11
static string Encode(byte[] Data)
Converts a binary block of data to a Base64URL-encoded string.
Definition: Base64Url.cs:48
Helps with parsing of commong data types.
Definition: CommonTypes.cs:15
static bool TryParse(string s, out double Value)
Tries to decode a string encoded double.
Definition: CommonTypes.cs:48
static string EncodeRfc822(DateTime Timestamp)
Encodes a date and time, according to RFC 822 §5.
Definition: CommonTypes.cs:669
Contains information about a response to a content request.
bool HasError
If an error occurred.
object Decoded
Decoded object.
Contains a markdown document. This markdown document class supports original markdown,...
async Task< string > GenerateHTML()
Generates HTML from the markdown text.
static Task< MarkdownDocument > CreateAsync(string MarkdownText, params Type[] TransparentExceptionTypes)
Contains a markdown document. This markdown document class supports original markdown,...
Contains settings that the Markdown parser uses to customize its behavior.
Helps with common XML-related tasks.
Definition: XML.cs:21
static string HtmlValueEncode(string s)
Differs from Encode(String), in that it does not encode the aposotrophe or the quote.
Definition: XML.cs:211
static string HtmlAttributeEncode(string s)
Differs from Encode(String), in that it does not encode the aposotrophe.
Definition: XML.cs:121
Basic authentication mechanism, as defined in RFC 2617: https://tools.ietf.org/html/rfc2617
static string ComputePasswordHash(string UserName, string Realm, string Password, string PasswordHashType)
Computs a password hash.
bool TryGetQueryParameter(string QueryParameter, out string Value)
Tries to get the value of an individual query parameter, if available.
Represents an HTTP request.
Definition: HttpRequest.cs:22
HttpRequestHeader Header
Request header.
Definition: HttpRequest.cs:182
bool HasData
If the request has data.
Definition: HttpRequest.cs:113
async Task< ContentResponse > DecodeDataAsync()
Decodes data sent in request.
Definition: HttpRequest.cs:139
string ResourceName
Name of resource.
Represets a response of an HTTP client request.
Definition: HttpResponse.cs:23
void SetHeader(string FieldName, string Value)
Sets a custom header field value.
Task Return(Exception ex)
Returns an error to the client.
async Task< string > RaiseCustomizeDeviceLoginForm(string Markdown)
Raises the CustomizeDeviceLoginForm event to customize a device login form before being returned to t...
void Register(OAuthAuthorizeResource? AuthorizeResource)
Registers an authorization resource.
static string GenerateRandomCode(int NrBytes)
Generates a random unique code.
string? LoginMasterFileName
File name to master file to use in generated login pages.
bool HasLoginMasterFileName
If a login master file name has been registered
async Task< string > RaiseCustomizeDeviceLoginReceipt(string Markdown)
Raises the CustomizeDeviceLoginReceipt event to customize a device login receipt before being returne...
OAUTH device authorization resource, as defined in RFC 8628. https://datatracker.ietf....
OAuthDeviceAuthorizationResource(OAuth2Environment Environment, string ResourceName)
OAUTH device authorization resource, as defined in RFC 8628.
OAuthDeviceAuthorizationResource(OAuth2Environment Environment)
OAUTH device authorization resource, as defined in RFC 8628.
const int MinimumIntervalSeconds
Minimum time between polling requests, in seconds. The device should not poll more frequently than th...
const string DefaultResourcePath
Default token resource path: /oauth/device
async Task GET(HttpRequest Request, HttpResponse Response)
Executes the POST method on the resource.
const string GrantType
Grant Type for device authorization flow.
async Task POST(HttpRequest Request, HttpResponse Response)
Executes the POST method on the resource.
Abstract base class for OAUTH resources.
IUserSource? Users
Data source for users, used to authenticate clients.
OAuth2Environment Environment
OAUTH2 environment, used to access clients, tokens, and other resources.
static Task ServiceUnavailable(HttpResponse Response, string ErrorCode, string ErrorDescription)
Returns a Service Unavailable error back to the client.
static bool IsValidScope(string Scope)
Checks if a scope value is valid, according to the OAUTH2 specification.
static Task BadRequest(HttpResponse Response, string ErrorCode, string ErrorDescription)
Returns a Bad Request error back to the client.
Provides OAUTH resource meta-data, as defined in RFC 9728. https://datatracker.ietf....
static StringBuilder GenerateServerUrl(HttpRequest Request, out int Port)
Generates a server URL, based on the request.
Implements an in-memory cache.
Definition: Cache.cs:17
Collection of variables.
Definition: Variables.cs:25
A factory that can create and validate JWT tokens.
Definition: JwtFactory.cs:66
string Create(params KeyValuePair< string, object >[] Claims)
Creates a new JWT token.
Definition: JwtFactory.cs:379
Contains information about a Java Web Token (JWT). JWT is defined in RFC 7519: https://tools....
Definition: JwtToken.cs:22
static bool TryParse(string Token, out JwtToken ParsedToken)
Tries to parse a JWT token.
Definition: JwtToken.cs:68
byte[] ComputeVariable(byte[] N)
Computes the SPONGE function, as defined in section 4 of NIST FIPS 202.
Definition: Keccak1600.cs:408
Implements the SHA3 SHAKE256 extendable-output functions, as defined in section 6....
Definition: SHAKE256.cs:9
GET Interface for HTTP resources.
POST Interface for HTTP resources.
A Thing Registry user source, supporting management of devices, with information about ownership.
Basic interface for a user.
Definition: IUser.cs:7
string PasswordHashType
Type of password hash. The empty stream means a clear-text password.
Definition: IUser.cs:44
string UserName
User Name.
Definition: IUser.cs:12
string PasswordHash
Password Hash
Definition: IUser.cs:36
Task< IUser > TryGetUser(string UserName)
Tries to get a user with a given user name.
A User that can participate in distributed operations, where the user is identified using a JWT token...
Definition: ImplTypes.g.cs:58