Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VCST-1415: Platform as authorization server #2809

Open
wants to merge 23 commits into
base: dev
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -325,11 +325,7 @@ public static string FirstCharToUpper(this string input)

public static bool IsValidEmail(this string input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}
return _emailRegex.IsMatch(input);
return input != null && _emailRegex.IsMatch(input);
}

public static string ToSnakeCase(this string name)
Expand Down
2 changes: 2 additions & 0 deletions src/VirtoCommerce.Platform.Core/PlatformOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,7 @@ public class PlatformOptions
/// Include null values when serializing Rest API objects.
/// </summary>
public bool IncludeOutputNullValues { get; set; } = true;

public string ApplicationCookieName { get; set; } = ".VirtoCommerce.Identity.Application";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Routing;

namespace VirtoCommerce.Platform.Web.ActionConstraints;

public sealed class FormValueRequiredAttribute : ActionMethodSelectorAttribute
{
private readonly string _name;
private readonly string[] _forbiddenMethods = ["GET", "HEAD", "DELETE", "TRACE"];

public FormValueRequiredAttribute(string name)
{
_name = name;
}

public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
{
var request = routeContext.HttpContext.Request;

if (_forbiddenMethods.Contains(request.Method, StringComparer.OrdinalIgnoreCase))
{
return false;
}

if (string.IsNullOrEmpty(request.ContentType))
{
return false;
}

if (!request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))
{
return false;
}

return !string.IsNullOrEmpty(request.Form[_name]);
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ public class OAuthAppsController : Controller
private readonly ISet<string> _defaultPermissions = new HashSet<string>
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Logout,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
OpenIddictConstants.Permissions.ResponseTypes.Code,
OpenIddictConstants.Permissions.Scopes.Email,
OpenIddictConstants.Permissions.Scopes.Profile,
};

public OAuthAppsController(OpenIddictApplicationManager<OpenIddictEntityFrameworkCoreApplication> manager)
Expand Down
12 changes: 12 additions & 0 deletions src/VirtoCommerce.Platform.Web/Model/AuthorizeViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;

namespace VirtoCommerce.Platform.Web.Model;

public class AuthorizeViewModel
{
[Display(Name = "Application")]
public string ApplicationName { get; set; }

[Display(Name = "Scope")]
public string Scope { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ public class PushNotificationUserIdProvider : IUserIdProvider
public virtual string GetUserId(HubConnectionContext connection)
{
// Return user name for compatibility with PushNotification.Creator
return connection.User.FindFirstValue(OpenIddictConstants.Claims.Subject);
return connection.User.FindFirstValue(OpenIddictConstants.Claims.Name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using OpenIddict.Validation.AspNetCore;
using VirtoCommerce.Platform.Core.Caching;
using VirtoCommerce.Platform.Core.Security;
using VirtoCommerce.Platform.Security.Authorization;
Expand Down Expand Up @@ -49,7 +50,7 @@ private Dictionary<string, AuthorizationPolicy> GetDynamicAuthorizationPoliciesF
{
resultLookup[permission.Name] = new AuthorizationPolicyBuilder().AddRequirements(new PermissionAuthorizationRequirement(permission.Name))
//Use the three schemas (JwtBearer, ApiKey and Basic) authentication for permission authorization policies.
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme, ApiKeyAuthenticationOptions.DefaultScheme, BasicAuthenticationOptions.DefaultScheme)
.AddAuthenticationSchemes(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme, ApiKeyAuthenticationOptions.DefaultScheme, BasicAuthenticationOptions.DefaultScheme)
.Build();
}
return resultLookup;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using VirtoCommerce.Platform.Core;
using VirtoCommerce.Platform.Core.Common;
using VirtoCommerce.Platform.Core.Events;
Expand Down Expand Up @@ -215,7 +216,7 @@ private bool TryGetUserInfo(ExternalLoginInfo externalLoginInfo, out string user
if (providerConfig?.Provider is not null)
{
userName = providerConfig.Provider.GetUserName(externalLoginInfo);
userEmail = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ??
userEmail = externalLoginInfo.Principal.FindFirstValue(OpenIddictConstants.Claims.Email) ??
(userName.IsValidEmail() ? userName : null);
}

Expand Down
107 changes: 74 additions & 33 deletions src/VirtoCommerce.Platform.Web/Security/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
using System;
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using NetEscapades.AspNetCore.SecurityHeaders.Headers.ContentSecurityPolicy;
using VirtoCommerce.Platform.Core.Common;
using VirtoCommerce.Platform.Core.Security;
using VirtoCommerce.Platform.Core.Security.Search;
Expand All @@ -19,6 +22,9 @@ namespace VirtoCommerce.Platform.Web.Security
{
public static class ServiceCollectionExtensions
{
private const string _scpFormActionUriKey = "Content-Security-Policy-Form-Action-Uri";
private static readonly ConcurrentDictionary<string, HeaderPolicyCollection> _policyCache = new();

public static IServiceCollection AddSecurityServices(this IServiceCollection services, Action<AuthorizationOptions> setupAction = null)
{
services.AddTransient<ISecurityRepository, SecurityRepository>();
Expand Down Expand Up @@ -120,44 +126,79 @@ public static void AddForwardedHeaders(this IServiceCollection services)
}
}

public static void UseCustomSecurityHeaders(this IApplicationBuilder app)
public static void AddCustomSecurityHeaders(this IServiceCollection services)
{
var policies = new HeaderPolicyCollection().AddDefaultSecurityHeaders();
var options = app.ApplicationServices.GetService<IOptions<SecurityHeadersOptions>>().Value;
services.AddSecurityHeaderPolicies()
.SetPolicySelector(context =>
{
var options = context.HttpContext.RequestServices.GetService<IOptions<SecurityHeadersOptions>>().Value;
var formActionUri = context.HttpContext.GetFormActionUri() ?? string.Empty;

if (_policyCache.TryGetValue(formActionUri, out var policies))
{
return policies;
}

policies = new HeaderPolicyCollection().AddDefaultSecurityHeaders();

if (options.FrameOptions.EqualsIgnoreCase("SameOrigin"))
{
policies.AddFrameOptionsSameOrigin();
}
else if (options.FrameOptions.EqualsIgnoreCase("Deny"))
{
policies.AddFrameOptionsDeny();
}
else if (!string.IsNullOrEmpty(options.FrameOptions))
{
policies.AddFrameOptionsSameOrigin(options.FrameOptions);
}

policies.AddContentSecurityPolicy(builder =>
{
builder.AddObjectSrc().None();
builder.AddFormAction().Self().Uri(formActionUri);

if (options.FrameAncestors.EqualsIgnoreCase("None"))
{
builder.AddFrameAncestors().None();
}
else if (options.FrameAncestors.EqualsIgnoreCase("Self"))
{
builder.AddFrameAncestors().Self();
}
else if (!string.IsNullOrEmpty(options.FrameAncestors))
{
builder.AddFrameAncestors().From(options.FrameAncestors);
}
});

_policyCache.AddOrUpdate(formActionUri, policies, (_, _) => policies);

return policies;
});
}

if (options.FrameOptions.EqualsIgnoreCase("SameOrigin"))
{
policies.AddFrameOptionsSameOrigin();
}
else if (options.FrameOptions.EqualsIgnoreCase("Deny"))
{
policies.AddFrameOptionsDeny();
}
else if (!string.IsNullOrEmpty(options.FrameOptions))
{
policies.AddFrameOptionsSameOrigin(options.FrameOptions);
}
public static void OverrideScpFormActionUri(this HttpContext httpContext, string uri)
{
httpContext.Items[_scpFormActionUriKey] = uri;
}

policies.AddContentSecurityPolicy(builder =>
{
builder.AddObjectSrc().None();
builder.AddFormAction().Self();
public static string GetFormActionUri(this HttpContext httpContext)
{
return httpContext.Items.TryGetValue(_scpFormActionUriKey, out var value)
? value as string
: null;
}

if (options.FrameAncestors.EqualsIgnoreCase("None"))
{
builder.AddFrameAncestors().None();
}
else if (options.FrameAncestors.EqualsIgnoreCase("Self"))
{
builder.AddFrameAncestors().Self();
}
else if (!string.IsNullOrEmpty(options.FrameAncestors))
{
builder.AddFrameAncestors().From(options.FrameAncestors);
}
});
public static T Uri<T>(this T builder, string uri) where T : CspDirectiveBuilder
{
if (!string.IsNullOrWhiteSpace(uri))
{
builder.Sources.Add(uri);
}

app.UseSecurityHeaders(policies);
return builder;
}
}
}
Loading
Loading