Skip to content

Commit

Permalink
VCST-1415: Add ability to use platform as OpenID authorization server (
Browse files Browse the repository at this point in the history
…#2809)

Co-authored-by: Artem Dudarev <[email protected]>
  • Loading branch information
tatarincev and artem-dudarev authored Nov 14, 2024
1 parent f637ccc commit 8b2321f
Show file tree
Hide file tree
Showing 19 changed files with 769 additions and 130 deletions.
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
Expand Up @@ -14,15 +14,15 @@ public static class ClaimsPrincipalExtensions

public static string GetUserId(this ClaimsPrincipal claimsPrincipal)
{
return GetClaimValue(claimsPrincipal, UserIdClaimTypes);
return claimsPrincipal.FindAnyValue(UserIdClaimTypes);
}

public static string GetUserName(this ClaimsPrincipal claimsPrincipal)
{
return GetClaimValue(claimsPrincipal, UserNameClaimTypes);
return claimsPrincipal.FindAnyValue(UserNameClaimTypes);
}

private static string GetClaimValue(ClaimsPrincipal claimsPrincipal, string[] claimTypes)
public static string FindAnyValue(this ClaimsPrincipal claimsPrincipal, IList<string> claimTypes)
{
if (claimsPrincipal != null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using OpenIddict.Abstractions;
using VirtoCommerce.Platform.Core.Security;

namespace VirtoCommerce.Platform.Security.ExternalSignIn
{
Expand All @@ -12,6 +15,11 @@ public interface IExternalSignInProvider

string GetUserName(ExternalLoginInfo externalLoginInfo);

string GetEmail(ExternalLoginInfo externalLoginInfo)
{
return externalLoginInfo.Principal.FindAnyValue([OpenIddictConstants.Claims.Email, ClaimTypes.Email]);
}

string GetUserType();
string[] GetUserRoles();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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 HasFormValueAttribute : ActionMethodSelectorAttribute
{
private readonly string _name;
private readonly string[] _allowedMethods = ["POST"];

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

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

return _allowedMethods.Contains(request.Method, StringComparer.OrdinalIgnoreCase) &&
!string.IsNullOrEmpty(request.ContentType) &&
request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) &&
!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
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ public SecurityController(
}

private UserManager<ApplicationUser> UserManager => _signInManager.UserManager;
private string CurrentUserName => User?.Identity?.Name;

private readonly string UserNotFound = "User not found.";
private readonly string UserForbiddenToEdit = "It is forbidden to edit this user.";
Expand Down Expand Up @@ -143,11 +142,12 @@ public async Task<ActionResult<SignInResult>> Login([FromBody] LoginRequest requ
/// </summary>
[HttpGet]
[Authorize]
[AllowAnonymous]
[Route("logout")]
[ProducesResponseType(typeof(void), StatusCodes.Status204NoContent)]
public async Task<ActionResult> Logout()
{
var user = await UserManager.FindByNameAsync(CurrentUserName);
var user = await GetCurrentUserAsync();
if (user != null)
{
await _signInManager.SignOutAsync();
Expand All @@ -166,15 +166,10 @@ public async Task<ActionResult> Logout()
[Route("currentuser")]
public async Task<ActionResult<UserDetail>> GetCurrentUser()
{
if (User.Identity?.IsAuthenticated != true)
{
return Ok(new { });
}

var user = await UserManager.FindByNameAsync(CurrentUserName);
var user = await GetCurrentUserAsync();
if (user == null)
{
return NotFound();
return Ok(new { });
}

var result = new UserDetail
Expand Down Expand Up @@ -454,7 +449,7 @@ public async Task<ActionResult<SecurityResult>> ChangeCurrentUserPassword([FromB
[Authorize(PlatformPermissions.SecurityUpdate)]
public async Task<ActionResult<SecurityResult>> ChangePassword([FromRoute] string userName, [FromBody] ChangePasswordRequest changePassword)
{
var currentUser = await UserManager.FindByNameAsync(CurrentUserName);
var currentUser = await GetCurrentUserAsync();
if (currentUser == null)
{
throw new PlatformException("Can't find current user.");
Expand Down Expand Up @@ -509,7 +504,7 @@ public async Task<ActionResult<SecurityResult>> ChangePassword([FromRoute] strin
[Authorize(PlatformPermissions.SecurityUpdate)]
public async Task<ActionResult<SecurityResult>> ResetPassword([FromRoute] string userName, [FromBody] ResetPasswordConfirmRequest resetPasswordConfirm)
{
var currentUser = await UserManager.FindByNameAsync(CurrentUserName);
var currentUser = await GetCurrentUserAsync();
if (currentUser == null)
{
throw new PlatformException("Can't find current user.");
Expand Down Expand Up @@ -668,12 +663,7 @@ public async Task<ActionResult> RequestPasswordReset(string loginOrEmail)
[AllowAnonymous]
public async Task<ActionResult<IdentityResult>> ValidatePassword([FromBody] string password)
{
ApplicationUser user = null;
if (User.Identity?.IsAuthenticated == true)
{
user = await UserManager.FindByNameAsync(User.Identity.Name);
}

var user = await GetCurrentUserAsync();
var result = await ValidatePassword(user, password);

return Ok(result);
Expand Down Expand Up @@ -824,7 +814,7 @@ public async Task<ActionResult<UserLockedResult>> PasswordChangeEnabled()
{
var result = new PasswordChangeEnabledResult(true);

var currentUser = await UserManager.FindByNameAsync(CurrentUserName);
var currentUser = await GetCurrentUserAsync();
if (currentUser?.IsAdministrator == true)
{
result.Enabled = _passwordOptions.PasswordChangeByAdminEnabled;
Expand Down Expand Up @@ -1061,6 +1051,17 @@ public async Task<ActionResult<bool>> VerifyUserToken([FromRoute] string userId,
return Ok(success);
}

private Task<ApplicationUser> GetCurrentUserAsync()
{
if (string.IsNullOrEmpty(User.Identity?.Name) ||
!User.Identity.IsAuthenticated)
{
return Task.FromResult<ApplicationUser>(null);
}

return UserManager.FindByNameAsync(User.Identity.Name);
}

private bool IsUserEditable(string userName)
{
return _securityOptions.NonEditableUsers?.FirstOrDefault(x => x.EqualsInvariant(userName)) == null;
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
@@ -1,6 +1,5 @@
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
using OpenIddict.Abstractions;
using VirtoCommerce.Platform.Core.Security;

namespace VirtoCommerce.Platform.Web.PushNotifications;

Expand All @@ -9,6 +8,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.GetUserName();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
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 +49,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
10 changes: 6 additions & 4 deletions src/VirtoCommerce.Platform.Web/Security/ExternalSignInService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Authentication;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -212,10 +211,13 @@ private bool TryGetUserInfo(ExternalLoginInfo externalLoginInfo, out string user
userEmail = string.Empty;

var providerConfig = GetExternalSigninProviderConfiguration(externalLoginInfo);
if (providerConfig?.Provider is not null)
var provider = providerConfig?.Provider;

if (provider is not null)
{
userName = providerConfig.Provider.GetUserName(externalLoginInfo);
userEmail = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ??
userName = provider.GetUserName(externalLoginInfo);

userEmail = provider.GetEmail(externalLoginInfo) ??
(userName.IsValidEmail() ? userName : null);
}

Expand Down
Loading

0 comments on commit 8b2321f

Please sign in to comment.