4using System.Threading.Tasks;
73 if (HasDuplicateQueryParameters(Request))
76 "Duplicate query parameters.");
83 "Missing response_type parameter.");
91 internal static bool HasDuplicateQueryParameters(
HttpRequest Request)
93 HashSet<string> Parameters =
new HashSet<string>();
97 if (Parameters.Contains(P.Key))
100 Parameters.Add(P.Key);
106 private async Task PrepareForm(
string ResponseType, IDictionary<string, string> Form,
109 if (!Form.TryGetValue(
"state", out
string State))
110 State =
string.Empty;
112 if (!Form.TryGetValue(
"scope", out
string Scope))
113 Scope =
string.Empty;
116 await
BadRequest(Response,
"invalid_scope",
"Invalid scope parameter.");
120 switch (ResponseType)
123 if (!Form.TryGetValue(
"client_id", out
string ClientId))
124 ClientId =
string.Empty;
126 if (!Form.TryGetValue(
"redirect_uri", out
string RedirectUri) ||
127 string.IsNullOrEmpty(RedirectUri))
130 "Missing or empty redirect_uri parameter.");
134 if (!Uri.TryCreate(RedirectUri, UriKind.Absolute, out Uri? RedirectUri2))
136 await
BadRequest(Response,
"invalid_redirect_uri",
137 "Invalid redirection URI.");
141 if (!RedirectUri.StartsWith(
"https://") && RedirectUri2.Host !=
"localhost")
144 "Callback URIs must use HTTPS URI scheme (unless localhost) to ensure secure communication.");
148 if (!Form.TryGetValue(
"code_challenge", out
string CodeChallenge))
149 CodeChallenge =
string.Empty;
151 if (!Form.TryGetValue(
"code_challenge_method", out
string CodeChallengeMethod) ||
152 string.IsNullOrEmpty(CodeChallengeMethod))
154 CodeChallengeMethod =
"plain";
157 if (CodeChallengeMethod !=
"plain" && CodeChallengeMethod !=
"S256")
160 "Unsupported code_challenge_method: " + CodeChallengeMethod);
164 Response.
SetHeader(
"Cache-Control",
"max-age=0, no-cache, no-store");
165 Response.
SetHeader(
"Pragma",
"no-cache");
167 await Response.
Return(await this.GenerateLoginForm(Response, ClientId,
168 RedirectUri, State, Scope, CodeChallenge, CodeChallengeMethod,
169 string.Empty, RedirectUri));
182 User = UserWithClaims;
187 if (Form.TryGetValue(
"client_id", out ClientId) &&
191 ClientId +
", user name in authenticated user: " +
User.
UserName,
194 await
Forbidden(Response,
"invalid_request",
"Invalid credentials.");
200 await
Forbidden(Response,
"access_denied",
201 "User lacks privilege: " + MissingPrivilege);
205 Response.
SetHeader(
"Cache-Control",
"max-age=0, no-cache, no-store");
206 Response.
SetHeader(
"Pragma",
"no-cache");
209 Request.
Encrypted,
this.JwtFactory, Scope);
212 3600, Scope,
this.JwtFactory.Issuer,
false,
User, Request));
219 "Authentication not enabled.");
242 if (Challenges is
null)
243 await
Forbidden(Response,
"access_denied",
"Access denied");
246 await
Unauthorized(Response,
"access_denied",
"Access denied",
253 if (
string.IsNullOrEmpty(ResponseType))
256 "Empty response_type.");
261 await
BadRequest(Response,
"unsupported_response_type",
262 "Unsupported response_type parameter: " + ResponseType);
273 private async Task<HtmlDocument> GenerateLoginForm(
HttpResponse Response,
274 string UserName,
string From,
string State,
string Scope,
string CodeChallenge,
275 string CodeChallengeMethod,
string ErrorMessage,
string RedirectUri)
277 StringBuilder Markdown =
new StringBuilder();
279 Markdown.AppendLine(
"Title: Login");
280 Markdown.AppendLine(
"Description: OAUTH login page.");
284 Markdown.Append(
"Master: ");
288 Markdown.Append(
"Date: ");
290 Markdown.AppendLine();
291 Markdown.AppendLine(
new string(
'=', 40));
292 Markdown.AppendLine();
294 Markdown.AppendLine(
"Login");
295 Markdown.AppendLine(
"========");
296 Markdown.AppendLine();
298 int i = RedirectUri.IndexOf(
"://");
300 string? Origin =
null;
304 int j = RedirectUri.IndexOf(
'/', i + 3);
307 Host = RedirectUri.Substring(i + 3, j - i - 3);
308 Origin = RedirectUri[..j];
312 i = RedirectUri.IndexOf(
'?');
319 if (!(ClientInfo is
null))
321 if (!
string.IsNullOrEmpty(ClientInfo.LogoUri))
323 Markdown.Append(
";
324 Markdown.Append(ClientInfo.LogoUri);
325 Markdown.AppendLine(
")");
328 Markdown.AppendLine();
330 if (!
string.IsNullOrEmpty(ClientInfo.ClientName))
332 if (!
string.IsNullOrEmpty(ClientInfo.ClientUri))
334 Markdown.Append(
"You have been requested to log in by [*");
335 Markdown.Append(ClientInfo.ClientName);
336 Markdown.Append(
"*](");
337 Markdown.Append(ClientInfo.ClientUri);
338 Markdown.Append(
"). ");
342 Markdown.Append(
"You have been requested to log in by *");
343 Markdown.Append(ClientInfo.ClientName);
344 Markdown.Append(
"*. ");
351 else if (!
string.IsNullOrEmpty(Host))
353 Markdown.Append(
"You have been requested to log in by an **unregistered** ");
354 Markdown.Append(
"remote service at `");
355 Markdown.Append(Host);
356 Markdown.Append(
"`. ");
360 Markdown.Append(
"You have been requested to log in to an **unregistered** ");
361 Markdown.Append(
"remote service. ");
364 Markdown.Append(
"If you trust this service, please log in below. ");
365 Markdown.AppendLine(
"Otherwise, close the window and ignore the request.");
366 Markdown.AppendLine();
369 new KeyValuePair<string, object>(
"redirect_uri", From),
370 new KeyValuePair<string, object>(
"state", State),
371 new KeyValuePair<string, object>(
"scope", Scope),
372 new KeyValuePair<string, object>(
"code_challenge", CodeChallenge),
373 new KeyValuePair<string, object>(
"code_challenge_method", CodeChallengeMethod));
375 Markdown.Append(
"<form id='LoginForm' action='");
377 Markdown.Append(
"' method='post'>");
378 Markdown.Append(
"<input type='hidden' name='p' value='");
380 Markdown.AppendLine(
"'/>");
381 Markdown.AppendLine();
383 Markdown.AppendLine(
"<p>");
384 Markdown.AppendLine(
"<label for='client_id'>User Name:</label> ");
385 Markdown.Append(
"<input id='client_id' name='client_id' type='text' autofocus autocomplete='username");
387 if (!
string.IsNullOrEmpty(UserName))
389 Markdown.Append(
"' value='");
393 Markdown.AppendLine(
"'/>");
394 Markdown.AppendLine(
"</p>");
395 Markdown.AppendLine();
397 Markdown.AppendLine(
"<p>");
398 Markdown.AppendLine(
"<label for='client_secret'>Password:</label> ");
399 Markdown.Append(
"<input id='client_secret' name='client_secret' type='password' ");
400 Markdown.AppendLine(
"autocomplete='current-password'/>");
401 Markdown.AppendLine(
"</p>");
402 Markdown.AppendLine();
404 if (!
string.IsNullOrEmpty(ErrorMessage))
406 Markdown.AppendLine(
"<p>");
407 Markdown.Append(
"<strong id='errorMessage'>");
409 Markdown.AppendLine(
"</strong>");
410 Markdown.AppendLine(
"</p>");
411 Markdown.AppendLine();
414 Markdown.AppendLine(
"<button type='submit'>Login</button>");
415 Markdown.AppendLine(
"</form>");
416 Markdown.AppendLine();
418 if (!
string.IsNullOrEmpty(ClientInfo?.TosUri))
420 Markdown.Append(
"[Terms of Service](");
421 Markdown.Append(ClientInfo.TosUri);
422 Markdown.AppendLine(
")");
425 if (!
string.IsNullOrEmpty(ClientInfo?.PolicyUri))
427 Markdown.Append(
"[Privacy Policy](");
428 Markdown.Append(ClientInfo.PolicyUri);
429 Markdown.AppendLine(
")");
432 if ((ClientInfo?.Contacts?.Length ?? 0) > 0)
434 foreach (
string Contact
in ClientInfo?.Contacts ?? Array.Empty<
string>())
436 if (!
string.IsNullOrEmpty(Contact))
438 Markdown.Append(
"[Contact](");
440 if (Contact.IndexOf(
':') < 0)
442 if (Contact.Contains(
'@'))
443 Markdown.Append(
"mailto:");
445 Markdown.Append(
"tel:");
448 Markdown.Append(Contact);
449 Markdown.AppendLine(
")");
464 Response.
SetHeader(
"X-Frame-Options",
"DENY");
465 Response.
SetHeader(
"Content-Security-Policy",
"frame-ancestors 'none'; " +
466 "default-src 'self'; script-src 'self'; object-src 'none'; " +
467 "base-uri 'none'; form-action 'self'" +
468 (
string.IsNullOrEmpty(Origin) ?
string.Empty :
" " + Origin));
483 await
BadRequest(Response,
"invalid_request",
"Missing payload.");
488 if (Content.
HasError || !(Content.
Decoded is Dictionary<string, string> Form))
491 "Expected URL-encoded WWW form.");
495 if (Form.TryGetValue(
"response_type", out
string ResponseType))
497 await this.PrepareForm(ResponseType, Form, Request, Response);
501 if (!Form.TryGetValue(
"client_id", out
string UserName) ||
502 !Form.TryGetValue(
"client_secret", out
string Password) ||
503 !Form.TryGetValue(
"p", out
string ParametersToken) ||
504 string.IsNullOrEmpty(ParametersToken) ||
506 !
this.JwtFactory.IsValid(Parameters) ||
507 !Parameters.TryGetClaim(
"redirect_uri", out
object Obj) || !(Obj is
string RedirectUri) ||
508 !Parameters.TryGetClaim(
"state", out Obj) || !(Obj is
string State) ||
509 !Parameters.TryGetClaim(
"scope", out Obj) || !(Obj is
string Scope) ||
510 !Parameters.TryGetClaim(
"code_challenge", out Obj) || !(Obj is
string CodeChallenge))
512 await
BadRequest(Response,
"invalid_request",
"Invalid form.");
516 if (!Parameters.TryGetClaim(
"code_challenge_method", out Obj) ||
517 !(Obj is
string CodeChallengeMethod) ||
518 string.IsNullOrWhiteSpace(CodeChallengeMethod))
520 CodeChallengeMethod =
"plain";
523 if (CodeChallengeMethod !=
"plain" && CodeChallengeMethod !=
"S256")
526 "Unsupported code_challenge_method: " + CodeChallengeMethod);
530 if (
string.IsNullOrEmpty(RedirectUri))
533 "Missing or empty redirect_uri parameter.");
540 this.
Users!, Request, this.
Realm ??
string.Empty);
544 await
Forbidden(Response,
"access_denied",
545 "User cannot authenticate via this interface.");
556 await Response.
Return(await this.GenerateLoginForm(Response,
557 UserName, RedirectUri, State, Scope, CodeChallenge,
558 CodeChallengeMethod,
"User cannot be used with OAUTH login.",
563 if (!
string.IsNullOrEmpty(Scope) &&
566 await Response.
Return(await this.GenerateLoginForm(Response,
567 UserName, RedirectUri, State, Scope, CodeChallenge,
568 CodeChallengeMethod,
"User does not have sufficient privileges to complete the request.",
574 Request.
Encrypted, CodeChallenge, CodeChallengeMethod, RedirectUri,
577 if (RedirectUri.Contains(
'?'))
578 RedirectUri +=
"&code=" + HttpUtility.UrlEncode(Code);
580 RedirectUri +=
"?code=" + HttpUtility.UrlEncode(Code);
582 if (!
string.IsNullOrEmpty(State))
583 RedirectUri +=
"&state=" + HttpUtility.UrlEncode(State);
593 await Response.
Return(await this.GenerateLoginForm(Response, UserName,
594 RedirectUri, State, Scope, CodeChallenge, CodeChallengeMethod,
595 "Invalid user name or password.", RedirectUri));
599 await
Forbidden(Response,
"access_denied",
"Password empty.");
603 await Response.
Return(await this.GenerateLoginForm(Response, UserName,
604 RedirectUri, State, Scope, CodeChallenge, CodeChallengeMethod,
605 "You are temporarily blocked. Try again after: " +
610 await Response.
Return(await this.GenerateLoginForm(Response, UserName,
611 RedirectUri, State, Scope, CodeChallenge, CodeChallengeMethod,
612 "You are permanently blocked.", RedirectUri));
Helps with parsing of commong data types.
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.
mTLS authentication mechanism, where identity is taken from a valid client certificate.
Base class for all HTTP authentication schemes, as defined in RFC-7235: https://datatracker....
abstract string[] GetChallenges(HttpRequest Request)
Gets available challenges for the authenticating client to respond to.
Represents an HTTP request.
HttpRequestHeader Header
Request header.
string RemoteEndPoint
Remote end-point.
bool HasData
If the request has data.
bool Encrypted
If the connection is encrypted or not.
IUser User
Authenticated user, if available, or null if not available.
async Task< ContentResponse > DecodeDataAsync()
Decodes data sent in request.
string ResourceName
Name of resource.
Represets a response of an HTTP client request.
async Task SendResponse()
Sends the response back to the client. If the resource is synchronous, there's no need to call this m...
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 a redirect URI using by an OAuth client.
string ClientId
OAuth 2.0 client identifier string.
Event arguments for implicit OAUTH authentication requests.
bool PermitMtlsAuthentication
If user identity can be implicitly authenticated using the client certificate used in mutual TLS auth...
IUserWithClaims? User
Implicitly authenticated user, if any. If null, no user was authenticated.
bool PermitWwwAuthentication
If implicit authentication can use result of WWW-Authenticate mechanism in HTTP to identify a user,...
Manages the OAuth 2 environment.
OAuthTokenResource TokenResource
Registered token resource
void Register(OAuthAuthorizeResource? AuthorizeResource)
Registers an authorization resource.
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 > RaiseCustomizeLoginForm(string Markdown)
Raises the CustomizeLoginForm event to customize a login form before being returned to the client.
OAUTH authorize resource, as defined in RFC 6749. https://datatracker.ietf.org/doc/html/rfc6749
const string DefaultResourcePath
Default authorize resource path: /oauth/authorize
EventHandlerAsync< ImplicitAuthenticationEventArgs >? ImplicitAuthenticationRequest
Event raised when an implicit authentication request is received.
OAuthAuthorizeResource(OAuth2Environment Environment)
OAUTH authorize resource, as defined in RFC 6749.
async Task POST(HttpRequest Request, HttpResponse Response)
Executes the POST method on the resource.
bool AllowsPOST
If the POST method is allowed.
async Task GET(HttpRequest Request, HttpResponse Response)
Executes the GET method on the resource.
OAuthAuthorizeResource(OAuth2Environment Environment, string ResourceName)
OAUTH authorize resource, as defined in RFC 6749.
bool AllowsGET
If the GET method is allowed.
Abstract base class for OAUTH resources.
OAuth2Environment Environment
OAUTH2 environment, used to access clients, tokens, and other resources.
string? Realm
Realm name, if any, used for authentication. Null if no realm is defined.
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.
bool InitAuthentication()
Initializes authentication schemes, if not already initialized.
static Task BadRequest(HttpResponse Response, string ErrorCode, string ErrorDescription)
Returns a Bad Request error back to the client.
static Task Unauthorized(HttpResponse Response, string ErrorCode, string ErrorDescription, string[] Challenges)
Returns an Unauthorized error back to the client.
static Task Forbidden(HttpResponse Response, string ErrorCode, string ErrorDescription)
Returns a Forbidden error back to the client.
static bool HasScopePrivileges(string Scopes, IUser User, [NotNullWhen(false)] out string? MissingPrivilege)
Checks if a user has the privileges associated with a set of scopes.
HttpAuthenticationScheme?[] AuthenticationSchemes
Available authentication schemes, if initialized.
OAUTH token resource, as defined in RFC 6749. https://datatracker.ietf.org/doc/html/rfc6749
The response to the request can be found under a different URI and SHOULD be retrieved using a GET me...
Static interface for database persistence. In order to work, a database provider has to be assigned t...
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 .
void AddRange(IEnumerable< T > Collection)
Adds a range of elements (last) to the list.
T[] ToArray()
Returns an array containing all elements of the collection.
A factory that can create and validate JWT tokens.
bool HasIssuer
If the factory has an issuer identifier.
string Issuer
Issuer identifier of the token factory, if available.
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.
Class that monitors login events, and help applications determine malicious intent....
static void Fail(string Message, string UserName, string RemoteEndPoint, string Protocol)
Handles a failed login attempt.
Contains information about a login attempt.
IUser User
User object corresponding to the successfully logged in user.
DateTime? Next
Time when a new login can be attempted.
LoginResultType Type
Type of login result.
Corresponds to a user in the system.
Maintains the collection of all users in the system.
GET Interface for HTTP resources.
POST Interface for HTTP resources.
A User that can participate in distributed operations, where the user is identified using a JWT token...
LoginResultType
Result of login attempt