Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/Exceptionless.Core/Configuration/OAuthServerOptions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Exceptionless.Core.Services;
using Microsoft.Extensions.Configuration;

namespace Exceptionless.Core.Configuration;
Expand All @@ -7,8 +8,11 @@ public class OAuthServerOptions
public TimeSpan AuthorizationCodeLifetime { get; internal set; } = TimeSpan.FromMinutes(5);
public TimeSpan AccessTokenLifetime { get; internal set; } = TimeSpan.FromHours(1);
public TimeSpan RefreshTokenLifetime { get; internal set; } = TimeSpan.FromDays(30);
public TimeSpan DeviceCodeLifetime { get; internal set; } = TimeSpan.FromMinutes(15);
public TimeSpan DeviceCodePollingInterval { get; internal set; } = TimeSpan.FromSeconds(5);
public bool EnableClientIdMetadataDocuments { get; internal set; } = true;
public int DynamicClientRegistrationIpLimit { get; internal set; } = 20;
public int DeviceAuthorizationIpLimit { get; internal set; } = 120;
public TimeSpan ClientMetadataDocumentCacheLifetime { get; internal set; } = TimeSpan.FromHours(1);
public TimeSpan ClientMetadataDocumentRequestTimeout { get; internal set; } = TimeSpan.FromSeconds(5);
public int ClientMetadataDocumentMaxBytes { get; internal set; } = 32 * 1024;
Expand All @@ -19,8 +23,11 @@ public static OAuthServerOptions ReadFromConfiguration(IConfiguration config)
options.AuthorizationCodeLifetime = TimeSpan.FromMinutes(config.GetValue("OAuthServer:AuthorizationCodeLifetimeMinutes", 5));
options.AccessTokenLifetime = TimeSpan.FromMinutes(config.GetValue("OAuthServer:AccessTokenLifetimeMinutes", 60));
options.RefreshTokenLifetime = TimeSpan.FromDays(config.GetValue("OAuthServer:RefreshTokenLifetimeDays", 30));
options.DeviceCodeLifetime = TimeSpan.FromMinutes(config.GetValue("OAuthServer:DeviceCodeLifetimeMinutes", 15));
options.DeviceCodePollingInterval = TimeSpan.FromSeconds(config.GetValue("OAuthServer:DeviceCodePollingIntervalSeconds", 5));
options.EnableClientIdMetadataDocuments = config.GetValue("OAuthServer:EnableClientIdMetadataDocuments", true);
options.DynamicClientRegistrationIpLimit = config.GetValue("OAuthServer:DynamicClientRegistrationIpLimit", 20);
options.DeviceAuthorizationIpLimit = config.GetValue("OAuthServer:DeviceAuthorizationIpLimit", 120);
options.ClientMetadataDocumentCacheLifetime = TimeSpan.FromMinutes(config.GetValue("OAuthServer:ClientMetadataDocumentCacheLifetimeMinutes", 60));
options.ClientMetadataDocumentRequestTimeout = TimeSpan.FromSeconds(config.GetValue("OAuthServer:ClientMetadataDocumentRequestTimeoutSeconds", 5));
options.ClientMetadataDocumentMaxBytes = config.GetValue("OAuthServer:ClientMetadataDocumentMaxBytes", 32 * 1024);
Expand All @@ -34,6 +41,7 @@ public record OAuthClientOptions
public required string Name { get; init; }
public IReadOnlyCollection<string> RedirectUris { get; init; } = [];
public IReadOnlyCollection<string> Scopes { get; init; } = [];
public IReadOnlyCollection<string> GrantTypes { get; init; } = [OAuthGrantTypes.AuthorizationCode, OAuthGrantTypes.RefreshToken];
public bool IsDisabled { get; init; }

internal OAuthClientOptions Normalize()
Expand All @@ -49,6 +57,11 @@ internal OAuthClientOptions Normalize()
.Where(s => !String.IsNullOrWhiteSpace(s))
.Select(s => s.Trim().ToLowerInvariant())
.Distinct(StringComparer.Ordinal)
.ToArray(),
GrantTypes = GrantTypes
.Where(g => !String.IsNullOrWhiteSpace(g))
.Select(g => g.Trim())
.Distinct(StringComparer.Ordinal)
.ToArray()
};
}
Expand Down
30 changes: 29 additions & 1 deletion src/Exceptionless.Core/Models/OAuthApplication.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Exceptionless.Core.Attributes;
using Exceptionless.Core.Authorization;
using Exceptionless.Core.Services;
using Foundatio.Repositories.Models;

namespace Exceptionless.Core.Models;
Expand All @@ -21,13 +22,17 @@ public class OAuthApplication : IIdentity, IHaveDates, IValidatableObject
public string Name { get; set; } = null!;

[Required]
[Length(1, 20)]
[Length(0, 20)]
public string[] RedirectUris { get; set; } = [];

[Required]
[Length(1, 20)]
public string[] Scopes { get; set; } = [];

[Required]
[Length(1, 3)]
public string[] GrantTypes { get; set; } = [OAuthGrantTypes.AuthorizationCode, OAuthGrantTypes.RefreshToken];

[MaxLength(1000)]
public string? Notes { get; set; }

Expand All @@ -44,6 +49,29 @@ public class OAuthApplication : IIdentity, IHaveDates, IValidatableObject

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
foreach (string _ in GrantTypes.Where(String.IsNullOrWhiteSpace))
yield return new ValidationResult("Grant type cannot be empty.", [nameof(GrantTypes)]);

foreach (string grantType in GrantTypes.Where(g => !String.IsNullOrWhiteSpace(g)))
{
if (!OAuthGrantTypes.SupportedGrantTypes.Contains(grantType, StringComparer.Ordinal))
yield return new ValidationResult($"'{grantType}' is not a supported OAuth grant type.", [nameof(GrantTypes)]);
}

bool supportsAuthorizationCode = GrantTypes.Contains(OAuthGrantTypes.AuthorizationCode, StringComparer.Ordinal);
bool supportsDeviceCode = GrantTypes.Contains(OAuthGrantTypes.DeviceCode, StringComparer.Ordinal);
if (!supportsAuthorizationCode && !supportsDeviceCode)
yield return new ValidationResult("OAuth applications must support authorization_code or device_code.", [nameof(GrantTypes)]);

if (GrantTypes.Contains(OAuthGrantTypes.RefreshToken, StringComparer.Ordinal) && !supportsAuthorizationCode && !supportsDeviceCode)
yield return new ValidationResult("The refresh_token grant type requires authorization_code or device_code.", [nameof(GrantTypes)]);

if (Scopes.Contains(AuthorizationRoles.OfflineAccess, StringComparer.Ordinal) && !GrantTypes.Contains(OAuthGrantTypes.RefreshToken, StringComparer.Ordinal))
yield return new ValidationResult("The offline_access scope requires the refresh_token grant type.", [nameof(Scopes)]);

if (supportsAuthorizationCode && RedirectUris.Length == 0)
yield return new ValidationResult("Redirect URIs are required for authorization_code clients.", [nameof(RedirectUris)]);

foreach (string _ in RedirectUris.Where(String.IsNullOrWhiteSpace))
yield return new ValidationResult("Redirect URI cannot be empty.", [nameof(RedirectUris)]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public override void ConfigureIndexMapping(TypeMappingDescriptor<OAuthApplicatio
.Keyword(e => e.ClientId)
.Keyword(e => e.RedirectUris)
.Keyword(e => e.Scopes)
.Keyword(e => e.GrantTypes)
.Keyword(e => e.CreatedByUserId)
.Keyword(e => e.UpdatedByUserId)
.Boolean(e => e.IsDisabled));
Expand Down
Loading
Loading