Skip to content

Commit 8b2321f

Browse files
VCST-1415: Add ability to use platform as OpenID authorization server (#2809)
Co-authored-by: Artem Dudarev <artem@virtoworks.com>
1 parent f637ccc commit 8b2321f

File tree

19 files changed

+769
-130
lines changed

19 files changed

+769
-130
lines changed

src/VirtoCommerce.Platform.Core/Extensions/StringExtensions.cs

+1-5
Original file line numberDiff line numberDiff line change
@@ -325,11 +325,7 @@ public static string FirstCharToUpper(this string input)
325325

326326
public static bool IsValidEmail(this string input)
327327
{
328-
if (input == null)
329-
{
330-
throw new ArgumentNullException(nameof(input));
331-
}
332-
return _emailRegex.IsMatch(input);
328+
return input != null && _emailRegex.IsMatch(input);
333329
}
334330

335331
public static string ToSnakeCase(this string name)

src/VirtoCommerce.Platform.Core/PlatformOptions.cs

+2
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,7 @@ public class PlatformOptions
7777
/// Include null values when serializing Rest API objects.
7878
/// </summary>
7979
public bool IncludeOutputNullValues { get; set; } = true;
80+
81+
public string ApplicationCookieName { get; set; } = ".VirtoCommerce.Identity.Application";
8082
}
8183
}

src/VirtoCommerce.Platform.Core/Security/ClaimsPrincipalExtensions.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ public static class ClaimsPrincipalExtensions
1414

1515
public static string GetUserId(this ClaimsPrincipal claimsPrincipal)
1616
{
17-
return GetClaimValue(claimsPrincipal, UserIdClaimTypes);
17+
return claimsPrincipal.FindAnyValue(UserIdClaimTypes);
1818
}
1919

2020
public static string GetUserName(this ClaimsPrincipal claimsPrincipal)
2121
{
22-
return GetClaimValue(claimsPrincipal, UserNameClaimTypes);
22+
return claimsPrincipal.FindAnyValue(UserNameClaimTypes);
2323
}
2424

25-
private static string GetClaimValue(ClaimsPrincipal claimsPrincipal, string[] claimTypes)
25+
public static string FindAnyValue(this ClaimsPrincipal claimsPrincipal, IList<string> claimTypes)
2626
{
2727
if (claimsPrincipal != null)
2828
{

src/VirtoCommerce.Platform.Security/ExternalSignIn/IExternalSignInProvider.cs

+8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
using System.Security.Claims;
12
using Microsoft.AspNetCore.Identity;
3+
using OpenIddict.Abstractions;
4+
using VirtoCommerce.Platform.Core.Security;
25

36
namespace VirtoCommerce.Platform.Security.ExternalSignIn
47
{
@@ -12,6 +15,11 @@ public interface IExternalSignInProvider
1215

1316
string GetUserName(ExternalLoginInfo externalLoginInfo);
1417

18+
string GetEmail(ExternalLoginInfo externalLoginInfo)
19+
{
20+
return externalLoginInfo.Principal.FindAnyValue([OpenIddictConstants.Claims.Email, ClaimTypes.Email]);
21+
}
22+
1523
string GetUserType();
1624
string[] GetUserRoles();
1725
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.Linq;
3+
using Microsoft.AspNetCore.Mvc.Abstractions;
4+
using Microsoft.AspNetCore.Mvc.ActionConstraints;
5+
using Microsoft.AspNetCore.Routing;
6+
7+
namespace VirtoCommerce.Platform.Web.ActionConstraints;
8+
9+
public sealed class HasFormValueAttribute : ActionMethodSelectorAttribute
10+
{
11+
private readonly string _name;
12+
private readonly string[] _allowedMethods = ["POST"];
13+
14+
public HasFormValueAttribute(string name)
15+
{
16+
_name = name;
17+
}
18+
19+
public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
20+
{
21+
var request = routeContext.HttpContext.Request;
22+
23+
return _allowedMethods.Contains(request.Method, StringComparer.OrdinalIgnoreCase) &&
24+
!string.IsNullOrEmpty(request.ContentType) &&
25+
request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) &&
26+
!string.IsNullOrEmpty(request.Form[_name]);
27+
}
28+
}

src/VirtoCommerce.Platform.Web/Controllers/Api/AuthorizationController.cs

+365-3
Large diffs are not rendered by default.

src/VirtoCommerce.Platform.Web/Controllers/Api/OAuthAppsController.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@ public class OAuthAppsController : Controller
2222
private readonly ISet<string> _defaultPermissions = new HashSet<string>
2323
{
2424
OpenIddictConstants.Permissions.Endpoints.Authorization,
25+
OpenIddictConstants.Permissions.Endpoints.Logout,
2526
OpenIddictConstants.Permissions.Endpoints.Token,
2627
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
27-
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials
28+
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
29+
OpenIddictConstants.Permissions.ResponseTypes.Code,
30+
OpenIddictConstants.Permissions.Scopes.Email,
31+
OpenIddictConstants.Permissions.Scopes.Profile,
2832
};
2933

3034
public OAuthAppsController(OpenIddictApplicationManager<OpenIddictEntityFrameworkCoreApplication> manager)

src/VirtoCommerce.Platform.Web/Controllers/Api/SecurityController.cs

+19-18
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ public SecurityController(
8080
}
8181

8282
private UserManager<ApplicationUser> UserManager => _signInManager.UserManager;
83-
private string CurrentUserName => User?.Identity?.Name;
8483

8584
private readonly string UserNotFound = "User not found.";
8685
private readonly string UserForbiddenToEdit = "It is forbidden to edit this user.";
@@ -143,11 +142,12 @@ public async Task<ActionResult<SignInResult>> Login([FromBody] LoginRequest requ
143142
/// </summary>
144143
[HttpGet]
145144
[Authorize]
145+
[AllowAnonymous]
146146
[Route("logout")]
147147
[ProducesResponseType(typeof(void), StatusCodes.Status204NoContent)]
148148
public async Task<ActionResult> Logout()
149149
{
150-
var user = await UserManager.FindByNameAsync(CurrentUserName);
150+
var user = await GetCurrentUserAsync();
151151
if (user != null)
152152
{
153153
await _signInManager.SignOutAsync();
@@ -166,15 +166,10 @@ public async Task<ActionResult> Logout()
166166
[Route("currentuser")]
167167
public async Task<ActionResult<UserDetail>> GetCurrentUser()
168168
{
169-
if (User.Identity?.IsAuthenticated != true)
170-
{
171-
return Ok(new { });
172-
}
173-
174-
var user = await UserManager.FindByNameAsync(CurrentUserName);
169+
var user = await GetCurrentUserAsync();
175170
if (user == null)
176171
{
177-
return NotFound();
172+
return Ok(new { });
178173
}
179174

180175
var result = new UserDetail
@@ -454,7 +449,7 @@ public async Task<ActionResult<SecurityResult>> ChangeCurrentUserPassword([FromB
454449
[Authorize(PlatformPermissions.SecurityUpdate)]
455450
public async Task<ActionResult<SecurityResult>> ChangePassword([FromRoute] string userName, [FromBody] ChangePasswordRequest changePassword)
456451
{
457-
var currentUser = await UserManager.FindByNameAsync(CurrentUserName);
452+
var currentUser = await GetCurrentUserAsync();
458453
if (currentUser == null)
459454
{
460455
throw new PlatformException("Can't find current user.");
@@ -509,7 +504,7 @@ public async Task<ActionResult<SecurityResult>> ChangePassword([FromRoute] strin
509504
[Authorize(PlatformPermissions.SecurityUpdate)]
510505
public async Task<ActionResult<SecurityResult>> ResetPassword([FromRoute] string userName, [FromBody] ResetPasswordConfirmRequest resetPasswordConfirm)
511506
{
512-
var currentUser = await UserManager.FindByNameAsync(CurrentUserName);
507+
var currentUser = await GetCurrentUserAsync();
513508
if (currentUser == null)
514509
{
515510
throw new PlatformException("Can't find current user.");
@@ -668,12 +663,7 @@ public async Task<ActionResult> RequestPasswordReset(string loginOrEmail)
668663
[AllowAnonymous]
669664
public async Task<ActionResult<IdentityResult>> ValidatePassword([FromBody] string password)
670665
{
671-
ApplicationUser user = null;
672-
if (User.Identity?.IsAuthenticated == true)
673-
{
674-
user = await UserManager.FindByNameAsync(User.Identity.Name);
675-
}
676-
666+
var user = await GetCurrentUserAsync();
677667
var result = await ValidatePassword(user, password);
678668

679669
return Ok(result);
@@ -824,7 +814,7 @@ public async Task<ActionResult<UserLockedResult>> PasswordChangeEnabled()
824814
{
825815
var result = new PasswordChangeEnabledResult(true);
826816

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

1054+
private Task<ApplicationUser> GetCurrentUserAsync()
1055+
{
1056+
if (string.IsNullOrEmpty(User.Identity?.Name) ||
1057+
!User.Identity.IsAuthenticated)
1058+
{
1059+
return Task.FromResult<ApplicationUser>(null);
1060+
}
1061+
1062+
return UserManager.FindByNameAsync(User.Identity.Name);
1063+
}
1064+
10641065
private bool IsUserEditable(string userName)
10651066
{
10661067
return _securityOptions.NonEditableUsers?.FirstOrDefault(x => x.EqualsInvariant(userName)) == null;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace VirtoCommerce.Platform.Web.Model;
4+
5+
public class AuthorizeViewModel
6+
{
7+
[Display(Name = "Application")]
8+
public string ApplicationName { get; set; }
9+
10+
[Display(Name = "Scope")]
11+
public string Scope { get; set; }
12+
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
using System.Security.Claims;
21
using Microsoft.AspNetCore.SignalR;
3-
using OpenIddict.Abstractions;
2+
using VirtoCommerce.Platform.Core.Security;
43

54
namespace VirtoCommerce.Platform.Web.PushNotifications;
65

@@ -9,6 +8,6 @@ public class PushNotificationUserIdProvider : IUserIdProvider
98
public virtual string GetUserId(HubConnectionContext connection)
109
{
1110
// Return user name for compatibility with PushNotification.Creator
12-
return connection.User.FindFirstValue(OpenIddictConstants.Claims.Subject);
11+
return connection.User.GetUserName();
1312
}
1413
}

src/VirtoCommerce.Platform.Web/Security/Authorization/PermissionAuthorizationPolicyProvider.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
using System.Collections.Generic;
22
using System.Threading.Tasks;
3-
using Microsoft.AspNetCore.Authentication.JwtBearer;
43
using Microsoft.AspNetCore.Authorization;
54
using Microsoft.Extensions.Configuration;
65
using Microsoft.Extensions.Options;
6+
using OpenIddict.Validation.AspNetCore;
77
using VirtoCommerce.Platform.Core.Caching;
88
using VirtoCommerce.Platform.Core.Security;
99
using VirtoCommerce.Platform.Security.Authorization;
@@ -49,7 +49,7 @@ private Dictionary<string, AuthorizationPolicy> GetDynamicAuthorizationPoliciesF
4949
{
5050
resultLookup[permission.Name] = new AuthorizationPolicyBuilder().AddRequirements(new PermissionAuthorizationRequirement(permission.Name))
5151
//Use the three schemas (JwtBearer, ApiKey and Basic) authentication for permission authorization policies.
52-
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme, ApiKeyAuthenticationOptions.DefaultScheme, BasicAuthenticationOptions.DefaultScheme)
52+
.AddAuthenticationSchemes(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme, ApiKeyAuthenticationOptions.DefaultScheme, BasicAuthenticationOptions.DefaultScheme)
5353
.Build();
5454
}
5555
return resultLookup;

src/VirtoCommerce.Platform.Web/Security/ExternalSignInService.cs

+6-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Security.Authentication;
5-
using System.Security.Claims;
65
using System.Threading.Tasks;
76
using Microsoft.AspNetCore.Identity;
87
using Microsoft.AspNetCore.Mvc;
@@ -212,10 +211,13 @@ private bool TryGetUserInfo(ExternalLoginInfo externalLoginInfo, out string user
212211
userEmail = string.Empty;
213212

214213
var providerConfig = GetExternalSigninProviderConfiguration(externalLoginInfo);
215-
if (providerConfig?.Provider is not null)
214+
var provider = providerConfig?.Provider;
215+
216+
if (provider is not null)
216217
{
217-
userName = providerConfig.Provider.GetUserName(externalLoginInfo);
218-
userEmail = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ??
218+
userName = provider.GetUserName(externalLoginInfo);
219+
220+
userEmail = provider.GetEmail(externalLoginInfo) ??
219221
(userName.IsValidEmail() ? userName : null);
220222
}
221223

0 commit comments

Comments
 (0)