Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
OAuthRegistrationResource.cs
1using System;
4using System.Threading.Tasks;
5using Waher.Content;
15
17{
24 {
28 public const string DefaultResourcePath = "/oauth/register";
29
36 {
37 }
38
45 string ResourceName)
47 {
49 }
50
54 public bool AllowsPOST => true;
55
62 public async Task POST(HttpRequest Request, HttpResponse Response)
63 {
64 if (!Request.HasData)
65 {
66 await BadRequest(Response, "invalid_request", "Missing payload.");
67 return;
68 }
69
70 ParsedRegistrationRequest? Parsed = await this.ParseRegistrationRequest(
71 Request, Response, false);
72
73 if (Parsed is null)
74 return;
75
76 RegistrationRequest RegistrationRequest = Parsed.Request;
77
78 IRegistration? Registration = await Parsed.DynamicUserSource.RegisterUser(
79 RegistrationRequest);
80
81 if (Registration is null)
82 {
83 await Forbidden(Response, "access_denied",
84 "Not permitted to register new client.");
85 return;
86 }
87
88 DateTime TP = DateTime.UtcNow;
90 {
91 ClientId = Registration.ClientId,
92 ClientSecretExpiresAt = Registration.ClientSecretExpiresAt,
93 AccessToken = OAuth2Environment.GenerateRandomCode(64),
94 Created = TP,
95 Updated = TP,
96 RemoteEndPoint = RegistrationRequest.RemoteEndPoint,
97 RedirectUris = RegistrationRequest.RedirectUris,
98 GrantTypes = RegistrationRequest.GrantTypes,
99 ResponseTypes = RegistrationRequest.ResponseTypes,
100 TokenEndpointAuthMethod = RegistrationRequest.TokenEndpointAuthMethod,
101 ClientName = RegistrationRequest.ClientName,
102 SoftwareId = RegistrationRequest.SoftwareId,
103 SoftwareVersion = RegistrationRequest.SoftwareVersion,
104 ClientUri = RegistrationRequest.ClientUri?.ToString(),
105 LogoUri = RegistrationRequest.LogoUri?.ToString(),
106 TosUri = RegistrationRequest.TosUri?.ToString(),
107 PolicyUri = RegistrationRequest.PolicyUri?.ToString(),
108 JwksUri = RegistrationRequest.JwksUri?.ToString(),
109 Scopes = RegistrationRequest.Scopes,
110 Contacts = RegistrationRequest.Contacts,
111 Jwks = RegistrationRequest.Jwks,
112 MetaData = RegistrationRequest.MetaData
113 };
114
115 await Database.Insert(ClientInfo);
116 await AddRedirectUrls(Registration.ClientId, RegistrationRequest.RedirectUris);
117
118 Dictionary<string, object> ResponseObj = this.RegistrationResponse(Request,
119 Response, Parsed, ClientInfo, Registration);
120
121 Response.StatusCode = 201;
122 Response.StatusMessage = "Created";
123
124 await Response.Return(ResponseObj);
125 }
126
127 internal Dictionary<string, object> RegistrationResponse(HttpRequest Request,
128 HttpResponse Response, ParsedRegistrationRequest? Parsed,
129 OAuthClientInformation ClientInfo, IRegistration? Registration)
130 {
131 Dictionary<string, object> ResponseObj = new Dictionary<string, object>();
132
133 if (!(Parsed is null))
134 {
135 foreach (KeyValuePair<string, object> P in Parsed.RequestObj)
136 ResponseObj[P.Key] = P.Value;
137 }
138
139 ResponseObj["client_id"] = ClientInfo.ClientId!;
140 ResponseObj["client_id_issued_at"] = (long)ClientInfo.Created.Subtract(JSON.UnixEpoch).TotalSeconds;
141
143 {
144 string RegistrationClientUri = Request.Header.GetURL(false, false).
146 "/" + ClientInfo.ObjectId;
147
148 ResponseObj["registration_access_token"] = ClientInfo.AccessToken!;
149 ResponseObj["registration_client_uri"] = RegistrationClientUri;
150 }
151
152 if (!(Registration is null) && (Parsed?.ReturnClientSecret ?? false))
153 ResponseObj["client_secret"] = Registration.ClientSecret;
154
155 if ((!(Registration is null) && (Parsed?.ReturnClientSecret ?? false)) ||
156 ClientInfo.ClientSecretExpiresAt.HasValue)
157 {
158 ResponseObj["client_secret_expires_at"] = ClientInfo.ClientSecretExpiresAt.HasValue ?
159 (long)ClientInfo.ClientSecretExpiresAt.Value.Subtract(JSON.UnixEpoch).TotalSeconds : 0L;
160 }
161
162 Response.SetHeader("Cache-Control", "max-age=0, no-cache, no-store");
163 Response.SetHeader("Pragma", "no-cache");
164
165 return ResponseObj;
166 }
167
168 internal static async Task AddRedirectUrls(string ClientId, string[]? RedirectUris)
169 {
170 if (!(RedirectUris is null))
171 {
172 foreach (string RedirectUri in RedirectUris)
173 {
174 OAuthRedirectUri? UriObj = new OAuthRedirectUri()
175 {
176 ClientId = ClientId,
177 Uri = RedirectUri
178 };
179
180 await Database.Insert(UriObj);
181 }
182 }
183 }
184
185 internal async Task<ParsedRegistrationRequest?> ParseRegistrationRequest(HttpRequest Request,
186 HttpResponse Response, bool PermitClientCredentials)
187 {
188 ContentResponse Decoded = await Request.DecodeDataAsync();
189 if (Decoded.HasError ||
190 !(Decoded.Decoded is Dictionary<string, object> RequestObj))
191 {
192 await BadRequest(Response, "invalid_request", "Invalid form.");
193 return null;
194 }
195
196 string[]? RedirectUris = null;
197 string[]? GrantTypes = null;
198 string[]? ResponseTypes = null;
199 string? TokenEndpointAuthMethod = null;
200 string? ClientName = null;
201 string? SoftwareId = null;
202 string? SoftwareVersion = null;
203 string? ClientId = null;
204 string? ClientSecret = null;
205 Uri? ClientUri = null;
206 Uri? LogoUri = null;
207 Uri? TosUri = null;
208 Uri? PolicyUri = null;
209 Uri? JwksUri = null;
210 string[]? Scopes = null;
211 string[]? Contacts = null;
212 Dictionary<string, object?>? Jwks = null;
213 Dictionary<string, object?>? MetaData = null;
214 bool ReturnClientSecret = false;
215
216 foreach (KeyValuePair<string, object> P in RequestObj)
217 {
218 switch (P.Key)
219 {
220 case "redirect_uris":
221 RedirectUris = ToStrings(P.Value);
222
223 if (!(RedirectUris is null))
224 {
225 foreach (string RedirectUri in RedirectUris)
226 {
227 if (!Uri.TryCreate(RedirectUri, UriKind.Absolute, out Uri Parsed) ||
228 !string.IsNullOrEmpty(Parsed.Fragment) ||
229 !string.IsNullOrEmpty(Parsed.Query))
230 {
231 await BadRequest(Response, "invalid_redirect_uri", "Invalid redirection URI.");
232 return null;
233 }
234
235 OAuthRedirectUri? UriObj = await Database.FindFirstIgnoreRest<OAuthRedirectUri>(
236 new FilterFieldEqualTo("Uri", RedirectUri));
237
238 if (!(UriObj is null))
239 {
240 if (ClientId is null &&
241 RequestObj.TryGetValue("client_id", out object Obj) &&
242 Obj is string ClientId3)
243 {
244 ClientId = ClientId3;
245 }
246
247 if (UriObj.ClientId != ClientId)
248 {
249 await BadRequest(Response, "invalid_client_metadata", "URI already registered.");
250 return null;
251 }
252 }
253 }
254 }
255 break;
256
257 case "grant_types":
258 GrantTypes = ToStrings(P.Value);
259 break;
260
261 case "response_types":
262 ResponseTypes = ToStrings(P.Value);
263 break;
264
265 case "contacts":
266 Contacts = ToStrings(P.Value);
267 break;
268
269 case "token_endpoint_auth_method":
270 TokenEndpointAuthMethod = P.Value?.ToString();
271
272 switch (TokenEndpointAuthMethod)
273 {
274 case "client_secret_post":
275 case "client_secret_basic":
276 ReturnClientSecret = true;
277 break;
278 }
279 break;
280
281 case "client_name":
282 ClientName = P.Value?.ToString();
283 break;
284
285 case "software_id":
286 SoftwareId = P.Value?.ToString();
287 break;
288
289 case "software_version":
290 SoftwareVersion = P.Value?.ToString();
291 break;
292
293 case "client_uri":
294 if (!Uri.TryCreate(P.Value?.ToString(), UriKind.Absolute, out ClientUri))
295 {
296 await BadRequest(Response, "invalid_request", "Invalid client_uri");
297 return null;
298 }
299
300 if (!await IsValidUri(ClientUri,
301 string.Join(", ", HtmlCodec.HtmlContentTypes)))
302 {
303 await BadRequest(Response, "invalid_request", "Invalid client_uri");
304 return null;
305 }
306 break;
307
308 case "logo_uri":
309 if (!Uri.TryCreate(P.Value?.ToString(), UriKind.Absolute, out LogoUri))
310 {
311 await BadRequest(Response, "invalid_request", "Invalid logo_uri");
312 return null;
313 }
314
315 if (!await IsValidUri(LogoUri,
316 string.Join(", ", ImageCodec.ImageContentTypes)))
317 {
318 await BadRequest(Response, "invalid_request", "Invalid logo_uri");
319 return null;
320 }
321 break;
322
323 case "tos_uri":
324 if (!Uri.TryCreate(P.Value?.ToString(), UriKind.Absolute, out TosUri))
325 {
326 await BadRequest(Response, "invalid_request", "Invalid tos_uri");
327 return null;
328 }
329
330 if (!await IsValidUri(TosUri,
331 string.Join(", ", HtmlCodec.HtmlContentTypes)))
332 {
333 await BadRequest(Response, "invalid_request", "Invalid tos_uri");
334 return null;
335 }
336 break;
337
338 case "policy_uri":
339 if (!Uri.TryCreate(P.Value?.ToString(), UriKind.Absolute, out PolicyUri))
340 {
341 await BadRequest(Response, "invalid_request", "Invalid policy_uri");
342 return null;
343 }
344
345 if (!await IsValidUri(PolicyUri,
346 string.Join(", ", HtmlCodec.HtmlContentTypes)))
347 {
348 await BadRequest(Response, "invalid_request", "Invalid policy_uri");
349 return null;
350 }
351 break;
352
353 case "jwks_uri":
354 if (!Uri.TryCreate(P.Value?.ToString(), UriKind.Absolute, out JwksUri))
355 {
356 await BadRequest(Response, "invalid_request", "Invalid jwks_uri");
357 return null;
358 }
359
360 if (!await IsValidUri(JwksUri, JsonCodec.DefaultContentType))
361 {
362 await BadRequest(Response, "invalid_request", "Invalid jwks_uri");
363 return null;
364 }
365 break;
366
367 case "scope":
368 string Scope = P.Value?.ToString() ?? string.Empty;
369 if (!IsValidScope(Scope))
370 {
371 await BadRequest(Response, "invalid_scope", "Invalid scope parameter.");
372 return null;
373 }
374
375 Scopes = Scope.Split(' ', StringSplitOptions.RemoveEmptyEntries);
376 break;
377
378 case "jwks":
379 if (P.Value is Dictionary<string, object?> Jwks2)
380 Jwks = Jwks2;
381 else
382 {
383 await BadRequest(Response, "invalid_request", "Invalid jwks");
384 return null;
385 }
386 break;
387
388 case "client_id":
389 if (!PermitClientCredentials || !(P.Value is string ClientId2))
390 {
391 await BadRequest(Response, "invalid_request", "Invalid request parameter: " + P.Key);
392 return null;
393 }
394
395 ClientId = ClientId2;
396 break;
397
398 case "client_secret":
399 if (!PermitClientCredentials || !(P.Value is string ClientSecret2))
400 {
401 await BadRequest(Response, "invalid_request", "Invalid request parameter: " + P.Key);
402 return null;
403 }
404
405 ClientSecret = ClientSecret2;
406 break;
407
408 case "client_id_issued_at":
409 case "client_secret_expires_at":
410 await BadRequest(Response, "invalid_request", "Invalid request parameter: " + P.Key);
411 return null;
412
413 default:
414 if (P.Key.EndsWith("_uri") && (
415 !(P.Value is string s) ||
416 !Uri.TryCreate(s, UriKind.Absolute, out Uri MetaUri) ||
417 !await IsValidUri(MetaUri, "*/*")))
418 {
419 await BadRequest(Response, "invalid_request", "Invalid URI: " + P.Key);
420 return null;
421 }
422
423 MetaData ??= new Dictionary<string, object?>();
424 MetaData[P.Key] = P.Value;
425 break;
426 }
427 }
428
429 if (!(GrantTypes is null) &&
430 Array.IndexOf(GrantTypes, "implicit") >= 0 &&
431 !(ResponseTypes is null) &&
432 Array.IndexOf(ResponseTypes, "token") < 0)
433 {
434 await BadRequest(Response, "invalid_client_metadata",
435 "Implicit grant_type requires token response_type.");
436 return null;
437 }
438
439 if (!(this.Users is IDynamicUserSource DynamicUserSource))
440 {
441 await ServiceUnavailable(Response, "server_error",
442 "Client registration service not available.");
443 return null;
444 }
445
446 RegistrationRequest RegistrationRequest = new RegistrationRequest(
447 Request.RemoteEndPoint.RemovePortNumber(), RedirectUris, GrantTypes,
448 ResponseTypes, TokenEndpointAuthMethod, ClientName, SoftwareId,
449 SoftwareVersion, ClientUri, LogoUri, TosUri, PolicyUri, JwksUri,
450 Scopes, Contacts, Jwks, MetaData, ClientId, ClientSecret);
451
452 return new ParsedRegistrationRequest(RegistrationRequest, RequestObj,
453 DynamicUserSource, ReturnClientSecret);
454 }
455
456 private static async Task<bool> IsValidUri(Uri Uri, string Accept)
457 {
458 try
459 {
460 ContentResponse Response = await InternetContent.GetAsync(Uri,
461 new KeyValuePair<string, string>("Accept", Accept));
462
463 return !Response.HasError;
464 }
465 catch (Exception)
466 {
467 return false;
468 }
469 }
470
471 internal class ParsedRegistrationRequest
472 {
473 public ParsedRegistrationRequest(RegistrationRequest Request,
474 Dictionary<string, object> RequestObj, IDynamicUserSource DynamicUserSource,
475 bool ReturnClientSecret)
476 {
477 this.Request = Request;
478 this.RequestObj = RequestObj;
479 this.DynamicUserSource = DynamicUserSource;
480 this.ReturnClientSecret = ReturnClientSecret;
481 }
482
483 public RegistrationRequest Request;
484 public Dictionary<string, object> RequestObj;
485 public IDynamicUserSource DynamicUserSource;
486 public bool ReturnClientSecret;
487 }
488
489 internal class RegistrationRequest : IRegistrationRequest
490 {
491 public RegistrationRequest(string RemoteEndPoint, string[]? RedirectUris,
492 string[]? GrantTypes, string[]? ResponseTypes,
493 string? TokenEndpointAuthMethod, string? ClientName, string? SoftwareId,
494 string? SoftwareVersion, Uri? ClientUri, Uri? LogoUri, Uri? TosUri,
495 Uri? PolicyUri, Uri? JwksUri, string[]? Scopes, string[]? Contacts,
496 Dictionary<string, object?>? Jwks, Dictionary<string, object?>? MetaData,
497 string? ClientId, string? ClientSecret)
498 {
499 this.RemoteEndPoint = RemoteEndPoint;
500 this.PublicClient = TokenEndpointAuthMethod == "none";
501 this.ConfidentialClient = !this.PublicClient;
502 this.RedirectUris = RedirectUris;
503 this.GrantTypes = GrantTypes;
504 this.ResponseTypes = ResponseTypes;
505 this.TokenEndpointAuthMethod = TokenEndpointAuthMethod;
506 this.ClientName = ClientName;
507 this.SoftwareId = SoftwareId;
508 this.SoftwareVersion = SoftwareVersion;
509 this.ClientUri = ClientUri;
510 this.LogoUri = LogoUri;
511 this.TosUri = TosUri;
512 this.PolicyUri = PolicyUri;
513 this.JwksUri = JwksUri;
514 this.Scopes = Scopes;
515 this.Contacts = Contacts;
516 this.Jwks = Jwks;
517 this.MetaData = MetaData;
518 this.ClientId = ClientId;
519 this.ClientSecret = ClientSecret;
520 }
521
522 public string RemoteEndPoint { get; }
523 public bool PublicClient { get; }
524 public bool ConfidentialClient { get; }
525 public string[]? RedirectUris { get; }
526 public string[]? GrantTypes { get; }
527 public string[]? ResponseTypes { get; }
528 public string? TokenEndpointAuthMethod { get; }
529 public string? ClientName { get; }
530 public string? SoftwareId { get; }
531 public string? SoftwareVersion { get; }
532 public string? ClientId { get; }
533 public string? ClientSecret { get; }
534 public Uri? ClientUri { get; }
535 public Uri? LogoUri { get; }
536 public Uri? TosUri { get; }
537 public Uri? PolicyUri { get; }
538 public Uri? JwksUri { get; }
539 public string[]? Scopes { get; }
540 public string[]? Contacts { get; }
541 public Dictionary<string, object?>? Jwks { get; }
542 public Dictionary<string, object?>? MetaData { get; }
543 }
544
545 private static string[]? ToStrings(object? Value)
546 {
547 if (Value is null)
548 return null;
549 else if (Value is string[] Strings)
550 return Strings;
551 else if (Value is IEnumerable Items)
552 {
554
555 foreach (object Item in Items)
556 {
557 if (Item is string s)
558 Result.Add(s);
559 else
560 Result.Add(Item.ToString());
561 }
562
563 return Result.ToArray();
564 }
565 else
566 return new string[] { Value.ToString() };
567 }
568 }
569}
Contains information about a response to a content request.
bool HasError
If an error occurred.
object Decoded
Decoded object.
HTML encoder/decoder.
Definition: HtmlCodec.cs:15
static readonly string[] HtmlContentTypes
HTML content types.
Definition: HtmlCodec.cs:31
Image encoder/decoder.
Definition: ImageCodec.cs:14
static readonly string[] ImageContentTypes
Image content types.
Definition: ImageCodec.cs:126
Static class managing encoding and decoding of internet content.
static Task< ContentResponse > GetAsync(Uri Uri, params KeyValuePair< string, string >[] Headers)
Gets a resource, given its URI.
Helps with common JSON-related tasks.
Definition: JSON.cs:16
static readonly DateTime UnixEpoch
Unix Date and Time epoch, starting at 1970-01-01T00:00:00Z
Definition: JSON.cs:20
const string DefaultContentType
application/json
Definition: JsonCodec.cs:20
string GetURL()
Gets an absolute URL for the request.
Represents an HTTP request.
Definition: HttpRequest.cs:22
HttpRequestHeader Header
Request header.
Definition: HttpRequest.cs:182
string RemoteEndPoint
Remote end-point.
Definition: HttpRequest.cs:243
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.
Contains information about an OAuth client, as defined in RFC 7591.
DateTime? ClientSecretExpiresAt
When the client secret expires, if one is defined.
DateTime Created
When the client information was created.
string? AccessToken
Access token required to update or delete the client information object.
string? ObjectId
Object ID of the client information object in the database.
Contains information about a redirect URI using by an OAuth client.
string ClientId
OAuth 2.0 client identifier string.
bool HasManagementResource
If the environment has a registered client management resource
void Register(OAuthAuthorizeResource? AuthorizeResource)
Registers an authorization resource.
static string GenerateRandomCode(int NrBytes)
Generates a random unique code.
OAUTH client management resource, as defined in RFCs 7591. https://datatracker.ietf....
const string DefaultResourcePath
OAUTH client management resource, as defined in RFCs 7591.
OAUTH dynamic registration resource, as defined in RFCs 7591 and 7592. https://datatracker....
async Task POST(HttpRequest Request, HttpResponse Response)
Executes the POST method on the resource.
OAuthRegistrationResource(OAuth2Environment Environment)
OAUTH dynamic registration resource, as defined in RFCs 7591 and 7592.
const string DefaultResourcePath
Default registration resource path: /oauth/register
OAuthRegistrationResource(OAuth2Environment Environment, string ResourceName)
OAUTH dynamic registration resource, as defined in RFCs 7591 and 7592.
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.
static Task Forbidden(HttpResponse Response, string ErrorCode, string ErrorDescription)
Returns a Forbidden error back to the client.
Static interface for database persistence. In order to work, a database provider has to be assigned t...
Definition: Database.cs:21
static async Task Insert(object Object)
Inserts an object into the default collection of the database.
Definition: Database.cs:97
This filter selects objects that have a named field equal to a given value.
A chunked list is a linked list of chunks of objects of type T .
Definition: ChunkedList.cs:54
void Add(T Item)
Adds an item to the collection.
Definition: ChunkedList.cs:272
T[] ToArray()
Returns an array containing all elements of the collection.
Static class managing binary representations of strings.
Definition: Strings.cs:10
POST Interface for HTTP resources.
A dynamic user source, supporting registering new users.
Dynamic client registration, as defined in RFC 7591.
Definition: IRegistration.cs:9
DateTime? ClientSecretExpiresAt
Time at which the client secret will expire
string ClientId
OAuth 2.0 client identifier string.
string ClientSecret
OAuth 2.0 client secret string.
Dynamic client registration request, as defined in RFC 7591.