3using System.Diagnostics.CodeAnalysis;
5using System.Threading.Tasks;
35 public const string GrantType =
"urn:ietf:params:oauth:grant-type:device_code";
85 UserCode =
string.Empty;
88 ClientId =
string.Empty;
90 string ErrorMessage =
string.Empty;
91 bool AlreadyResponded =
false;
93 if (!
string.IsNullOrEmpty(UserCode))
95 if (!codes.TryGetValue(UserCode, out DeviceRef DeviceRef))
96 ErrorMessage =
"Invalid User Code";
97 else if (DeviceRef.Result.HasValue)
99 AlreadyResponded =
true;
101 if (DeviceRef.Result.Value)
102 ErrorMessage =
"Authorization request has already been accepted.";
104 ErrorMessage =
"Authorization request has already been declined.";
108 await Response.
Return(await this.GenerateAuthorizationForm(Request, Response,
109 ClientId, UserCode,
string.Empty,
false,
false, AlreadyResponded,
123 await
BadRequest(Response,
"invalid_request",
"No payload in request.");
128 if (Content.
HasError || !(Content.
Decoded is Dictionary<string, string> Form))
131 "Expected URL-encoded WWW form.");
138 "Device authorization service not available.");
142 if (Form.TryGetValue(
"user_code", out
string UserCode))
144 if (!Form.TryGetValue(
"p", out
string ParametersToken) ||
145 string.IsNullOrEmpty(ParametersToken) ||
147 !
this.JwtFactory.IsValid(Parameters) ||
148 !Parameters.TryGetClaim(
"client_id", out
object Obj) ||
149 !(Obj is
string ClientId))
151 await
BadRequest(Response,
"invalid_request",
"Missing or invalid parameters provided.");
155 if (!Form.TryGetValue(
"UserName", out
string UserName))
156 UserName =
string.Empty;
158 if (!Form.TryGetValue(
"Password", out
string Password))
159 Password =
string.Empty;
161 if (!Form.TryGetValue(
"Accept", out
string s) ||
167 if (!Form.TryGetValue(
"Decline", out s) ||
175 bool AlreadyResponded =
false;
177 if (
string.IsNullOrEmpty(UserCode))
178 s =
"Missing User Code";
179 else if (!codes.TryGetValue(UserCode, out DeviceRef DeviceRef) ||
180 DeviceRef.UserCode != UserCode)
182 s =
"Invalid User Code";
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.";
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,
201 s =
"Invalid user name, password, or owner.";
203 else if (DeviceRef.Result.HasValue)
205 AlreadyResponded =
true;
207 if (DeviceRef.Result.Value)
208 s =
"Authorization request has already been accepted.";
210 s =
"Authorization request has already been declined.";
214 DeviceRef.Result = Accept;
215 await Response.
Return(await this.GenerateResult(Request, Response, Accept));
219 await Response.
Return(await this.GenerateAuthorizationForm(Request, Response,
220 ClientId, UserCode, UserName, Accept, Decline, AlreadyResponded, s));
222 else if (Form.TryGetValue(
"client_id", out
string ClientId))
224 if (
string.IsNullOrEmpty(ClientId))
226 await
BadRequest(Response,
"invalid_request",
"Empty client_id.");
230 IUser? Device = await ThingRegistry.TryGetUser(ClientId);
233 await
ServiceUnavailable(Response,
"access_denied",
"Device or owner not registered.");
239 await
ServiceUnavailable(Response,
"access_denied",
"Device cannot be used in this interface.");
243 IUser? Owner = await ThingRegistry.TryGetOwner(Device);
246 await
ServiceUnavailable(Response,
"access_denied",
"Device or owner not registered.");
250 if (!Form.TryGetValue(
"scope", out
string Scope))
251 Scope =
string.Empty;
254 await
BadRequest(Response,
"invalid_scope",
"Invalid scope parameter.");
267 codes.ContainsKey(UserCode) ||
268 codes.ContainsKey(DeviceCode));
270 DeviceRef Ref =
new DeviceRef(DeviceClaims, Owner, Scope, DeviceCode, UserCode);
271 codes.Add(UserCode, Ref);
272 codes.Add(DeviceCode, Ref);
274 Response.
SetHeader(
"Cache-Control",
"max-age=0, no-cache, no-store");
275 Response.
SetHeader(
"Pragma",
"no-cache");
279 string VerificationUrl = sb.ToString();
281 sb.Append(
"?user_code=");
284 sb.Append(
"&client_id=");
287 string VerificationUrlComplete = sb.ToString();
289 await Response.
Return(
new Dictionary<string, object>()
291 {
"device_code", DeviceCode },
292 {
"user_code", UserCode },
293 {
"verification_uri", VerificationUrl },
294 {
"verification_uri_complete", VerificationUrlComplete },
295 {
"expires_in", 3600 },
300 await
BadRequest(Response,
"invalid_request",
"Missing or invalid parameters provided.");
303 private static string ComputeUserCode(
string DeviceCode,
string ClientId,
304 string OwnerUserName,
string OwnerPasswordHash)
306 StringBuilder sb =
new StringBuilder();
307 sb.Append(DeviceCode);
311 sb.Append(OwnerUserName);
313 sb.Append(OwnerPasswordHash);
319 internal bool TryGetDeviceReference(
string DeviceCode,
320 [NotNullWhen(
true)] out DeviceRef? Reference)
322 if (!codes.TryGetValue(DeviceCode, out Reference))
325 if (Reference.DeviceCode != DeviceCode)
334 internal class DeviceRef
337 string DeviceCode,
string UserCode)
339 this.Device = Device;
342 this.DeviceCode = DeviceCode;
343 this.UserCode = UserCode;
348 codes.Remove(this.DeviceCode);
349 codes.Remove(this.UserCode);
353 public DateTime? LastPoll =
null;
356 public string DeviceCode;
357 public string UserCode;
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)
365 StringBuilder Markdown =
new StringBuilder();
367 Markdown.AppendLine(
"Title: Device Authorization");
368 Markdown.AppendLine(
"Description: OAUTH device authorization page.");
372 Markdown.Append(
"Master: ");
376 Markdown.Append(
"Date: ");
378 Markdown.AppendLine();
379 Markdown.AppendLine(
new string(
'=', 40));
380 Markdown.AppendLine();
382 Markdown.AppendLine(
"Device Authorization");
383 Markdown.AppendLine(
"=======================");
384 Markdown.AppendLine();
385 Markdown.Append(
"A Device");
387 if (!
string.IsNullOrEmpty(ClientId))
389 Markdown.Append(
" with ID `");
390 Markdown.Append(ClientId);
391 Markdown.Append(
"`");
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();
400 Markdown.Append(
"<form id='AuthorizationForm' action='");
402 Markdown.Append(
"' method='post'>");
403 Markdown.AppendLine();
405 if (!AlreadyResponded)
408 new KeyValuePair<string, object>(
"client_id", ClientId));
410 Markdown.Append(
"<input type='hidden' name='p' value='");
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");
418 if (!
string.IsNullOrEmpty(UserCode))
420 Markdown.Append(
"' value='");
424 Markdown.AppendLine(
"'/>");
425 Markdown.AppendLine(
"</p>");
426 Markdown.AppendLine();
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.'");
432 Markdown.Append(
" checked");
433 Markdown.AppendLine(
"/>");
434 Markdown.AppendLine(
"<label for='Accept'>Accept authorization.</label> ");
435 Markdown.AppendLine(
"</p>");
436 Markdown.AppendLine();
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.'");
442 Markdown.Append(
" checked");
443 Markdown.AppendLine(
"/>");
444 Markdown.AppendLine(
"<label for='Decline'>Decline authorization.</label> ");
445 Markdown.AppendLine(
"</p>");
446 Markdown.AppendLine();
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))
453 Markdown.Append(
"' value='");
456 Markdown.AppendLine(
"'/>");
457 Markdown.AppendLine(
"</p>");
458 Markdown.AppendLine();
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();
468 if (!
string.IsNullOrEmpty(ErrorMessage))
470 Markdown.AppendLine(
"<p>");
471 Markdown.Append(
"<strong id='errorMessage'>");
473 Markdown.AppendLine(
"</strong>");
474 Markdown.AppendLine(
"</p>");
475 Markdown.AppendLine();
478 if (!AlreadyResponded)
479 Markdown.AppendLine(
"<button type='submit'>Submit</button>");
481 Markdown.AppendLine(
"</form>");
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'");
499 private async Task<HtmlDocument> GenerateResult(
HttpRequest Request,
502 StringBuilder Markdown =
new StringBuilder();
504 Markdown.AppendLine(
"Title: Accepted");
505 Markdown.AppendLine(
"Description: OAUTH device authorization has been accepted.");
509 Markdown.Append(
"Master: ");
513 Markdown.Append(
"Date: ");
515 Markdown.AppendLine();
516 Markdown.AppendLine(
new string(
'=', 40));
517 Markdown.AppendLine();
520 Markdown.AppendLine(
"Accepted");
522 Markdown.AppendLine(
"Declined");
524 Markdown.AppendLine(
"===========");
525 Markdown.AppendLine();
527 Markdown.Append(
"Authorization request has been ");
531 Markdown.AppendLine(
"accepted.");
532 Markdown.AppendLine(
"The device will be informed and granted access.");
536 Markdown.AppendLine(
"declined.");
537 Markdown.AppendLine(
"The device will be informed access has been denied.");
540 Markdown.AppendLine(
"You can safely close this tab.");
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'");
Static class that does BASE64URL encoding (using URL and filename safe alphabet), as defined in RFC46...
static string Encode(byte[] Data)
Converts a binary block of data to a Base64URL-encoded string.
Helps with parsing of commong data types.
static bool TryParse(string s, out double Value)
Tries to decode a string encoded double.
static string EncodeRfc822(DateTime Timestamp)
Encodes a date and time, according to RFC 822 §5.
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.
static string HtmlValueEncode(string s)
Differs from Encode(String), in that it does not encode the aposotrophe or the quote.
static string HtmlAttributeEncode(string s)
Differs from Encode(String), in that it does not encode the aposotrophe.
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.
Represents an HTTP request.
HttpRequestHeader Header
Request header.
bool HasData
If the request has data.
async Task< ContentResponse > DecodeDataAsync()
Decodes data sent in request.
string ResourceName
Name of resource.
Represets a response of an HTTP client request.
void SetHeader(string FieldName, string Value)
Sets a custom header field value.
Task Return(Exception ex)
Returns an error to the client.
Manages the OAuth 2 environment.
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....
bool AllowsPOST
If the POST method is allowed.
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.
bool AllowsGET
If the GET method is allowed.
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.
Implements an in-memory cache.
A factory that can create and validate JWT tokens.
string Create(params KeyValuePair< string, object >[] Claims)
Creates a new JWT token.
Contains information about a Java Web Token (JWT). JWT is defined in RFC 7519: https://tools....
static bool TryParse(string Token, out JwtToken ParsedToken)
Tries to parse a JWT token.
byte[] ComputeVariable(byte[] N)
Computes the SPONGE function, as defined in section 4 of NIST FIPS 202.
Implements the SHA3 SHAKE256 extendable-output functions, as defined in section 6....
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.
string PasswordHashType
Type of password hash. The empty stream means a clear-text password.
string UserName
User Name.
string PasswordHash
Password Hash
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...