3using System.Diagnostics.CodeAnalysis;
5using System.Threading.Tasks;
65 string CodeChallenge,
string CodeChallengeMethod,
string RedirectUri,
68 string Token = await this.CreateToken(
User,
Encrypted, Scope);
69 string Code = this.GenerateRandomCode();
71 codes[Code] =
new TokenRef(Token,
User, CodeChallenge, CodeChallengeMethod,
72 RedirectUri, 3600, Scope);
81 private string GenerateRandomCode()
90 codes.ContainsKey(Code) ||
91 refreshTokens.ContainsKey(Code) ||
92 usedRefreshTokens.ContainsKey(Code));
97 private class TokenRef
100 string CodeChallengeMethod,
string RedirectUri,
int ExpiresIn,
string Scope)
104 this.CodeChallenge = CodeChallenge;
105 this.CodeChallengeMethod = CodeChallengeMethod;
106 this.RedirectUri = RedirectUri;
107 this.ExpiresIn = ExpiresIn;
112 public string CodeChallenge;
113 public string CodeChallengeMethod;
114 public string RedirectUri;
117 public int ExpiresIn;
119 public async Task<bool> Check(
string CodeVerifier,
HttpResponse Response)
121 switch (this.CodeChallengeMethod)
124 if (CodeVerifier != this.CodeChallenge)
126 await
Forbidden(Response,
"invalid_grant",
127 "Invalid code_verifier.");
136 if (ExpectedCodeChallenge != this.CodeChallenge)
138 await
Forbidden(Response,
"invalid_grant",
139 "Invalid code_verifier.");
146 "Unsupported code_challenge_method: " + this.CodeChallengeMethod);
165 "Duplicate query parameters.");
171 await
BadRequest(Response,
"invalid_request",
"Missing code.");
175 if (!codes.TryGetValue(Code, out TokenRef Ref))
177 await
Forbidden(Response,
"invalid_grant",
"Invalid code.");
181 if (!
string.IsNullOrEmpty(Ref.CodeChallenge))
185 await
BadRequest(Response,
"invalid_request",
"Missing code_verifier.");
189 if (!await Ref.Check(CodeVerifier, Response))
195 Response.
SetHeader(
"Cache-Control",
"max-age=0, no-cache, no-store");
196 Response.
SetHeader(
"Pragma",
"no-cache");
198 await Response.
Return(this.TokenResponse(Ref.Token,
null, Ref.ExpiresIn,
199 Ref.Scope,
this.JwtFactory.Issuer,
true, Ref.User, Request));
212 await
BadRequest(Response,
"invalid_request",
"No payload in request.");
217 if (Content.
HasError || !(Content.
Decoded is Dictionary<string, string> Form))
220 "Expected URL-encoded WWW form.");
224 if (!Form.TryGetValue(
"grant_type", out
string GrantType))
226 await
BadRequest(Response,
"invalid_request",
"Missing grant_type.");
230 if (!Form.TryGetValue(
"scope", out
string Scope))
231 Scope =
string.Empty;
234 await
BadRequest(Response,
"invalid_scope",
"Invalid scope parameter.");
241 TokenFamily? TokenFamily =
null;
242 bool IssueRefreshToken =
true;
246 case "authorization_code":
247 if (!Form.TryGetValue(
"code", out
string Code))
249 await
BadRequest(Response,
"invalid_request",
"Missing code.");
253 if (!codes.TryGetValue(Code, out TokenRef Ref))
255 await
Forbidden(Response,
"invalid_grant",
"Invalid code.");
259 if (!Form.TryGetValue(
"redirect_uri", out
string RedirectUri))
261 await
BadRequest(Response,
"invalid_request",
"Missing redirect_uri.");
265 if (!TryGetClientId(Request, Form, out ClientId))
267 await
BadRequest(Response,
"invalid_request",
"Missing client_id.");
271 if (ClientId != Ref.User.UserName)
273 await
Forbidden(Response,
"access_denied",
"Access denied");
277 if (Ref.RedirectUri != RedirectUri)
279 await
Forbidden(Response,
"access_denied",
"Access denied");
283 if (!
string.IsNullOrEmpty(Ref.CodeChallenge))
285 if (!Form.TryGetValue(
"code_verifier", out
string CodeVerifier))
287 await
BadRequest(Response,
"invalid_request",
"Missing code_verifier.");
291 if (!await Ref.Check(CodeVerifier, Response))
301 case "client_credentials":
303 string ClientSecret =
string.Empty;
304 string InvalidGrantCode;
307 if (GrantType ==
"password")
309 InvalidGrantCode =
"invalid_grant";
310 HasCredentials = Form.TryGetValue(
"username", out ClientId) &&
311 Form.TryGetValue(
"password", out ClientSecret);
315 InvalidGrantCode =
"invalid_client";
316 IssueRefreshToken =
false;
318 if (Request.
User is
null)
320 HasCredentials = Form.TryGetValue(
"client_id", out ClientId) &&
321 Form.TryGetValue(
"client_secret", out ClientSecret);
325 if (Form.ContainsKey(
"client_id") ||
326 Form.ContainsKey(
"client_secret"))
329 "Invalid request parameters.");
340 User = UserWithClaims;
341 Token = await this.CreateToken(UserWithClaims, Request.
Encrypted, Scope);
350 await
Forbidden(Response,
"invalid_request",
351 "Request must be performed over an encrypted connection.");
357 await
Forbidden(Response,
"invalid_request",
358 "Cipher strength too weak.");
365 this.
Users!, Request, this.
Realm ??
string.Empty);
369 await
Forbidden(Response, InvalidGrantCode,
370 "User cannot authenticate via this interface.");
382 await
Forbidden(Response, InvalidGrantCode,
383 "Invalid client_id or client_secret.");
387 await
Forbidden(Response, InvalidGrantCode,
388 "No or empty client_secret.");
392 await
Forbidden(Response, InvalidGrantCode,
393 "Temporarily blocked. Try again after: " +
398 await
Forbidden(Response, InvalidGrantCode,
399 "Permanently blocked.");
410 User = UserWithClaims;
411 Token = await this.CreateToken(UserWithClaims, Request.
Encrypted, Scope);
416 "Missing credentials.");
422 await
Forbidden(Response,
"access_denied",
423 "User lacks privilege: " + MissingPrivilege);
428 case "refresh_token":
429 if (!Form.TryGetValue(
"refresh_token", out
string RefreshToken))
432 "Missing refresh_token.");
436 if (!refreshTokens.TryGetValue(RefreshToken, out TokenFamily))
438 if (usedRefreshTokens.TryGetValue(RefreshToken, out TokenFamily))
440 string Message =
"Attempt to reuse refresh token. Has the token leaked? Deprecating all associated tokens.";
445 Log.
Alert(Message, TokenFamily.User.UserName,
449 foreach (
string Token2
in TokenFamily.Tokens)
455 usedRefreshTokens.Remove(RefreshToken);
458 await
Forbidden(Response,
"access_denied",
459 "Invalid refresh_token.");
463 if (!TryGetClientId(Request, Form, out ClientId))
465 await
BadRequest(Response,
"invalid_request",
"Missing client_id.");
469 if (!TokenFamily.CanUseRefreshToken(ClientId, Request))
471 await
Forbidden(Response,
"access_denied",
"Access denied");
475 if (
string.IsNullOrEmpty(Scope))
476 Scope = (TokenFamily.Scopes?.Length ?? 0) == 0 ?
string.Empty :
string.Join(
' ', TokenFamily.Scopes);
479 string[] NewScopes = Scope.Split(
' ', StringSplitOptions.RemoveEmptyEntries);
481 foreach (
string Scope2
in NewScopes)
483 if (Array.IndexOf(TokenFamily.Scopes, Scope2) < 0)
485 await
Forbidden(Response,
"invalid_scope",
486 "Not permitted to escalate scope.");
491 TokenFamily.Scopes = NewScopes;
494 refreshTokens.Remove(RefreshToken);
495 usedRefreshTokens.Add(RefreshToken, TokenFamily);
498 Token = await this.CreateToken(
User, Request.
Encrypted, Scope);
505 "Device authorization not configured.");
509 if (!Form.TryGetValue(
"device_code", out
string DeviceCode))
511 await
BadRequest(Response,
"invalid_request",
"Missing device_code.");
515 if (!TryGetClientId(Request, Form, out ClientId))
517 await
BadRequest(Response,
"invalid_request",
"Missing client_id.");
524 await
Forbidden(Response,
"expired_token",
"Invalid device_code, or token has expired.");
528 if (ClientId != DeviceReference.Device.UserName)
530 await
Forbidden(Response,
"access_denied",
"Invalid client_id.");
534 DateTime TP = DateTime.UtcNow;
536 if (DeviceReference.LastPoll.HasValue &&
537 TP.Subtract(DeviceReference.LastPoll.Value).TotalSeconds <
540 await
BadRequest(Response,
"slow_down",
"Polling too fast. Slow down.");
544 DeviceReference.LastPoll = TP;
546 if (!DeviceReference.Result.HasValue)
548 await
BadRequest(Response,
"authorization_pending",
"Authorization has not yet been granted by owner.");
552 if (!DeviceReference.Result.Value)
554 await
Forbidden(Response,
"access_denied",
"Access has been denied by owner.");
560 await
Forbidden(Response,
"access_denied",
561 "Owner lacks privilege: " + MissingPrivilege);
565 User = DeviceReference.Device;
566 Scope = DeviceReference.Scope;
568 Token = await this.CreateToken(
User, Request.
Encrypted, Scope);
570 DeviceReference.Remove();
574 await
BadRequest(Response,
"unsupported_grant_type",
575 "Unsupported grant_type: " + GrantType);
579 Response.
SetHeader(
"Cache-Control",
"max-age=0, no-cache, no-store");
580 Response.
SetHeader(
"Pragma",
"no-cache");
582 await Response.
Return(this.TokenResponse(Token,
null, 3600,
584 Request, TokenFamily));
587 private static bool TryGetClientId(
HttpRequest Request, Dictionary<string, string> Form,
590 if (Form.TryGetValue(
"client_id", out ClientId))
598 s = Encoding.UTF8.GetString(Convert.FromBase64String(s[6..]));
599 int i = s.IndexOf(
':');
602 ClientId = s.Substring(0, i);
607 ClientId =
string.Empty;
620 if (
string.IsNullOrEmpty(Scope))
629 internal static async Task<LoginResult?> DoLogin(
string UserName,
string Password,
632 if (
string.IsNullOrEmpty(Password))
648 new KeyValuePair<string, object>(
"UserName", UserName));
656 if (HashBytes.HasValue)
659 if (PasswordHash == ExpectedHash)
671 internal Dictionary<string, object> TokenResponse(
string Token,
672 string? State,
int ExpiresIn,
string Scope,
string? Issuer,
675 return this.TokenResponse(Token, State, ExpiresIn, Scope, Issuer,
676 IssueRefreshToken,
User, Request,
null);
679 private Dictionary<string, object> TokenResponse(
string Token,
680 string? State,
int ExpiresIn,
string Scope,
string? Issuer,
682 TokenFamily? TokenFamily)
684 Dictionary<string, object> Result =
new Dictionary<string, object>()
686 {
"access_token", Token },
687 {
"token_type",
"Bearer" },
688 {
"expires_in", ExpiresIn }
691 if (!
string.IsNullOrEmpty(State))
692 Result[
"state"] = State;
694 if (!
string.IsNullOrEmpty(Issuer))
695 Result[
"iss"] = Issuer;
699 if (
string.IsNullOrEmpty(Scope))
700 Scopes = Array.Empty<
string>();
703 Result[
"scope"] = Scope;
704 Scopes = Scope.Split(
' ', StringSplitOptions.RemoveEmptyEntries);
707 if (IssueRefreshToken)
709 if (TokenFamily is
null)
710 TokenFamily =
new TokenFamily(Token, Scopes,
User, Request);
711 else if (!TokenFamily.CanUseRefreshToken(
User.
UserName, Request))
714 TokenFamily.Add(Token);
716 string RefreshToken = this.GenerateRandomCode();
717 refreshTokens[RefreshToken] = TokenFamily;
719 Result[
"refresh_token"] = RefreshToken;
725 private class TokenFamily
729 public IEnumerable<string> Tokens => this.tokens;
732 public string[] Scopes {
get;
set; }
733 public bool HasRemoteCertificate {
get; }
735 public string RemoteCertificateSerialNumber {
get; }
742 this.FirstRequest = FirstRequest;
743 this.Scopes = Scopes;
744 this.HasRemoteCertificate = !(this.FirstRequest.RemoteCertificate is
null);
745 this.RemoteEndpoint = this.FirstRequest.
RemoteEndPoint.RemovePortNumber();
746 this.RemoteCertificateSerialNumber =
751 public void Add(
string Token) => this.tokens.Add(Token);
753 public bool CanUseRefreshToken(
string ClientId,
HttpRequest Request)
758 if (this.HasRemoteCertificate)
764 this.RemoteCertificateSerialNumber)
785 public async Task<KeyValuePair<OAuthTokenType?, JwtToken?>>
TryGetTokenType(
string Token)
787 if (refreshTokens.ContainsKey(Token))
788 return new KeyValuePair<OAuthTokenType?, JwtToken?>(
OAuthTokenType.RefreshToken,
null);
789 else if (usedRefreshTokens.ContainsKey(Token))
790 return new KeyValuePair<OAuthTokenType?, JwtToken?>(
OAuthTokenType.ExpiredRefreshToken,
null);
791 else if (codes.ContainsKey(Token))
792 return new KeyValuePair<OAuthTokenType?, JwtToken?>(
OAuthTokenType.AccessCode,
null);
797 if (!
string.IsNullOrEmpty(ParsedToken.Subject) &&
798 !(
this.Users is
null) &&
799 await
this.Users.TryGetUser(ParsedToken.Subject) is
null)
801 return new KeyValuePair<OAuthTokenType?, JwtToken?>(
OAuthTokenType.ExpiredAccessToken,
null);
804 return new KeyValuePair<OAuthTokenType?, JwtToken?>(
OAuthTokenType.AccessToken, ParsedToken);
808 return new KeyValuePair<OAuthTokenType?, JwtToken?>(
OAuthTokenType.ExpiredAccessToken,
null);
811 return new KeyValuePair<OAuthTokenType?, JwtToken?>(
null,
null);
814 return new KeyValuePair<OAuthTokenType?, JwtToken?>(
null,
null);
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.
Contains information about a response to a content request.
bool HasError
If an error occurred.
object Decoded
Decoded object.
Static class managing the application event log. Applications and services log events on this static ...
static void Alert(string Message, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, string StackTrace, params KeyValuePair< string, object >[] Tags)
Logs an alert event.
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.
Digest authentication mechanism, as defined in RFC 2617: https://tools.ietf.org/html/rfc2617
static string EnsureHex(string s, byte NrBytes)
Tries to make sure s is a hexadecimal string. If it is a base64-encoded string, it is converted to a...
The server understood the request, but is refusing to fulfill it. Authorization will not help and the...
static ForbiddenException AccessDenied(string ObjectId, string ActorId)
Returns a ForbiddenException object, and logs a entry in the event log about the event.
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.
int CipherStrength
Cipher strength
X509Certificate RemoteCertificate
Remote client certificate, if any, associated with the request.
async Task< ContentResponse > DecodeDataAsync()
Decodes data sent in request.
HttpServer Server
HTTP Server receiving the 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.
ILoginAuditor LoginAuditor
Reference to login-auditor to help remove malicious users from the server.
int[] OpenHttpsPorts
HTTPS Ports successfully opened.
Manages the OAuth 2 environment.
OAuthDeviceAuthorizationResource DeviceAuthorizationResource
Registered device authorization resource
void Register(OAuthAuthorizeResource? AuthorizeResource)
Registers an authorization resource.
static string GenerateRandomCode(int NrBytes)
Generates a random unique code.
bool HasDeviceAuthorizationResource
If the environment has a registered device authorization resource
JwtFactory JwtFactory
Registered JWT factory
OAUTH authorize resource, as defined in RFC 6749. https://datatracker.ietf.org/doc/html/rfc6749
OAUTH device authorization resource, as defined in RFC 8628. https://datatracker.ietf....
const int MinimumIntervalSeconds
Minimum time between polling requests, in seconds. The device should not poll more frequently than th...
const string GrantType
Grant Type for device authorization flow.
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.
bool Encrypted
If TLS-encryption is enabled.
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 bool HasScopePrivileges(string Scopes, IUser User, [NotNullWhen(false)] out string? MissingPrivilege)
Checks if a user has the privileges associated with a set of scopes.
OAUTH token resource, as defined in RFC 6749. https://datatracker.ietf.org/doc/html/rfc6749
bool AllowsGET
If the GET method is allowed.
const string DefaultResourcePath
Default token resource path: /oauth/token
OAuthTokenResource(OAuth2Environment Environment, string ResourceName)
OAUTH token resource, as defined in RFC 6749.
bool AllowsPOST
If the POST method is allowed.
async Task GET(HttpRequest Request, HttpResponse Response)
Executes the POST method on the resource.
async Task< KeyValuePair< OAuthTokenType?, JwtToken?> > TryGetTokenType(string Token)
Tries to get the type of a token.
async Task POST(HttpRequest Request, HttpResponse Response)
Executes the POST method on the resource.
OAuthTokenResource(OAuth2Environment Environment)
OAUTH token resource, as defined in RFC 6749.
Implements an in-memory cache.
A chunked list is a linked list of chunks of objects of type T .
Contains methods for simple hash calculations.
static byte[] ComputeSHA256Hash(byte[] Data)
Computes the SHA-256 hash of a block of binary data.
Static class containing predefined JWT claim names.
const string Scope
Scope Values
A factory that can create and validate JWT tokens.
static void Deprecate(JwtToken Token)
Deprecates a token.
bool IsValid(JwtToken Token)
Checks if a token is valid and signed by the factory.
string Issuer
Issuer identifier of the token factory, if available.
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 async Task< KeyValuePair< string, object >[]> Annotate(string RemoteEndPoint, params KeyValuePair< string, object >[] Tags)
Annotates a remote endpoint.
static async void Success(string Message, string UserName, string RemoteEndPoint, string Protocol, params KeyValuePair< string, object >[] Tags)
Handles a successful login attempt.
static void Fail(string Message, string UserName, string RemoteEndPoint, string Protocol)
Handles a failed login attempt.
Login state information relating to a remote endpoint
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.
async Task< string > CreateToken(JwtFactory Factory, bool Encrypted, params KeyValuePair< string, object >[] AdditionalClaims)
Creates a JWT Token referencing the user object.
string PasswordHashType
Type of password hash. The empty stream means a clear-text password.
User()
Corresponds to a user in the system.
string PasswordHash
Password Hash
Maintains the collection of all users in the system.
async Task< IUser > TryGetUser(string UserName)
Tries to get a user with a given user name.
GET Interface for HTTP resources.
POST Interface for HTTP resources.
Task< DateTime?> GetEarliestLoginOpportunity(string RemoteEndPoint, string Protocol)
Checks when a remote endpoint can login.
Basic interface for a user.
string UserName
User Name.
Interface for data sources containing users.
A User that can participate in distributed operations, where the user is identified using a JWT token...
OAuthTokenType
Type of OAuth token.
Reason
Reason a token is not valid.
LoginResultType
Result of login attempt