I am using Identity Server 4 with MongoDB to create an identity portal.
services.AddIdentityServer().AddMongoRepository()
.AddMongoDbForAspIdentity<ApplicationUser, IdentityRole>(Configuration)
.AddClients()
.AddIdentityApiResources()
.AddPersistedGrants()
.AddDeveloperSigningCredential();
app.UseMongoDbForIdentityServer();
app.UseIdentityServer();
Here is the Mongo Db Repository
namespace IdentityServer.Extension
{
public static class IdentityServerBuilderExtensions
{/// <summary>
/// Adds mongo repository (mongodb) for IdentityServer
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IIdentityServerBuilder AddMongoRepository(this IIdentityServerBuilder builder)
{
builder.Services.AddTransient<IRepository, Repository>();
return builder;
}
/// <summary>
/// Adds mongodb implementation for the "Asp Net Core Identity" part (saving user and roles)
/// </summary>
/// <remarks><![CDATA[
/// Contains implemenations for
/// - IUserStore<T>
/// - IRoleStore<T>
/// ]]></remarks>
public static IIdentityServerBuilder AddMongoDbForAspIdentity<TIdentity, TRole>(this IIdentityServerBuilder builder, IConfigurationRoot configuration) where
TIdentity : ApplicationUser where TRole : Microsoft.AspNetCore.Identity.MongoDB.IdentityRole
{
//User Mongodb for Asp.net identity in order to get users stored
var configurationOptions = configuration.Get<MongoDbConfigurationOptions>();
var client = new MongoClient(configurationOptions.MongoConnection);
var database = client.GetDatabase(configurationOptions.MongoDatabaseName);
// Configure Asp Net Core Identity / Role to use MongoDB
builder.Services.AddSingleton<IUserStore<TIdentity>>(x =>
{
var usersCollection = database.GetCollection<TIdentity>("Identity_Users");
IndexChecks.EnsureUniqueIndexOnNormalizedEmail(usersCollection);
IndexChecks.EnsureUniqueIndexOnNormalizedUserName(usersCollection);
return new UserStore<TIdentity>(usersCollection);
});
builder.Services.AddSingleton<IRoleStore<TRole>>(x =>
{
var rolesCollection = database.GetCollection<TRole>("Identity_Roles");
IndexChecks.EnsureUniqueIndexOnNormalizedRoleName(rolesCollection);
return new RoleStore<TRole>(rolesCollection);
});
builder.Services.AddIdentity<TIdentity, TRole>().AddDefaultTokenProviders();
return builder;
}
/// <summary>
/// Configure ClientId / Secrets
/// </summary>
/// <param name="builder"></param>
/// <param name="configurationOption"></param>
/// <returns></returns>
public static IIdentityServerBuilder AddClients(this IIdentityServerBuilder builder)
{
builder.Services.AddTransient<IClientStore, CustomClientStore>();
builder.Services.AddTransient<ICorsPolicyService, InMemoryCorsPolicyService>();
return builder;
}
/// <summary>
/// Configure API & Resources
/// Note: Api's have also to be configured for clients as part of allowed scope for a given clientID
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IIdentityServerBuilder AddIdentityApiResources(this IIdentityServerBuilder builder)
{
builder.Services.AddTransient<IResourceStore, CustomResourceStore>();
return builder;
}
/// <summary>
/// Configure Grants
/// </summary>
/// <param name="builder">The builder.</param>
/// <returns></returns>
public static IIdentityServerBuilder AddPersistedGrants(this IIdentityServerBuilder builder)
{
builder.Services.TryAddSingleton<IPersistedGrantStore, CustomPersistedGrantStore>();
return builder;
}
}
}
Account Controller
private readonly SignInManager _signInManager; private readonly UserManager _userManager;
public AccountController(
IIdentityServerInteractionService interaction,
IClientStore clientStore,
IAuthenticationSchemeProvider schemeProvider,
IEventService events, UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
{
_signInManager = signInManager;
_userManager = userManager;
_interaction = interaction;
_clientStore = clientStore;
_schemeProvider = schemeProvider;
_events = events;
}
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password,
model.RememberLogin, lockoutOnFailure: true);
if (result.Succeeded)
{
var user = await _userManager.FindByNameAsync(model.UserName);
await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.Email, clientId: context?.ClientId));
// only set explicit expiration here if user chooses "remember me".
// otherwise we rely upon expiration configured in cookie middleware.
AuthenticationProperties props = null;
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
};
// issue authentication cookie with subject ID and username
await HttpContext.SignInAsync(user.Id, user.UserName, props);
if (context != null)
{
if (await _clientStore.IsPkceClientAsync(context.ClientId))
{
// if the client is PKCE then we assume it's native, so this change in how to
// return the response is for better UX for the end user.
return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
}
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
return Redirect(model.ReturnUrl);
}
// request for a local page
if (Url.IsLocalUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
else if (string.IsNullOrEmpty(model.ReturnUrl))
{
return Redirect("~/");
}
else
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
}
await _events.RaiseAsync(new UserLoginFailureEvent(model.UserName, "invalid credentials", clientId:context?.ClientId));
ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
}
Exception when running this line of code
var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password,model.RememberLogin, lockoutOnFailure: false);
Exception Error
System.InvalidOperationException: sub claim is missing
at IdentityServer4.Hosting.IdentityServerAuthenticationService.AssertRequiredClaims(ClaimsPrincipal principal)
at IdentityServer4.Hosting.IdentityServerAuthenticationService.AugmentPrincipal(ClaimsPrincipal principal)
at IdentityServer4.Hosting.IdentityServerAuthenticationService.SignInAsync(HttpContext context, String scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
at Microsoft.AspNetCore.Identity.SignInManager`1.SignInWithClaimsAsync(TUser user, AuthenticationProperties authenticationProperties, IEnumerable`1 additionalClaims)
at Microsoft.AspNetCore.Identity.SignInManager`1.SignInOrTwoFactorAsync(TUser user, Boolean isPersistent, String loginProvider, Boolean bypassTwoFactor)
at Microsoft.AspNetCore.Identity.SignInManager`1.PasswordSignInAsync(TUser user, String password, Boolean isPersistent, Boolean lockoutOnFailure)
at Microsoft.AspNetCore.Identity.SignInManager`1.PasswordSignInAsync(String userName, String password, Boolean isPersistent, Boolean lockoutOnFailure)
at IdentityServer.AccountController.Login(LoginInputModel model, String button) in /Users/macbook/Projects/IdentityPortal/IdentityServer/Quickstart/Account/AccountController.cs:line 116
First, while you are adding a client to indentityserver, you must give the client some allowed scopes, one of them is Profile:
new Client
{
ClientId = "userjs",
ClientName = "",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
//ClientUri= $"{clientsUrl["UserSpa"]}",
RedirectUris = { $"{clientsUrl["UserSpa"]}/" },
RequireConsent = false,
PostLogoutRedirectUris = { $"{clientsUrl["UserSpa"]}/" },
AllowedCorsOrigins = { $"{clientsUrl["UserSpa"]}" },
AllowedScopes =
{
"club",
IdentityServerConstants.StandardScopes.OpenId,
**IdentityServerConstants.StandardScopes.Profile,**
},
RequireClientSecret = false
},
Second, Add AddProfileService extension method after IdentityServer to services:
services.AddIdentityServer().AddMongoRepository()
.AddMongoDbForAspIdentity<ApplicationUser, IdentityRole>
(Configuration)
.AddClients()
.AddIdentityApiResources()
.AddPersistedGrants()
.AddDeveloperSigningCredential()
.AddProfileService<ProfileService>();
Third, implement IProfileService:
public class ProfileService : IProfileService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IServiceCollection _services;
private readonly ApplicationDbContext _context;
private CalcAllowedPermissions _calcAllowedPermissions;
public ProfileService(UserManager<ApplicationUser> userManager, ApplicationDbContext context)
{
_services = new ServiceCollection();
var sp = _services.BuildServiceProvider();
_userManager = userManager;
_context = context ?? throw new ArgumentNullException(nameof(context));
_calcAllowedPermissions = new CalcAllowedPermissions(_context);
}
**//This method is called whenever claims about the user are requested (e.g. during token creation or via the userinfo endpoint)**
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject));
var subjectId = subject.Claims.Where(x => x.Type == "sub").FirstOrDefault().Value;
var user = await _userManager.FindByIdAsync(subjectId);
if (user == null)
throw new ArgumentException("Invalid subject identifier");
var claims = GetClaimsFromUser(user,subject);
context.IssuedClaims = claims.Result.ToList();
}
public async Task IsActiveAsync(IsActiveContext context)
{
var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject));
var subjectId = subject.Claims.Where(x => x.Type == "sub").FirstOrDefault().Value;
var user = await _userManager.FindByIdAsync(subjectId);
context.IsActive = false;
if (user != null)
{
if (_userManager.SupportsUserSecurityStamp)
{
var security_stamp = subject.Claims.Where(c => c.Type == "security_stamp").Select(c => c.Value).SingleOrDefault();
if (security_stamp != null)
{
var db_security_stamp = await _userManager.GetSecurityStampAsync(user);
if (db_security_stamp != security_stamp)
return;
}
}
context.IsActive =
!user.LockoutEnabled ||
!user.LockoutEnd.HasValue ||
user.LockoutEnd <= DateTime.Now;
}
}
private async Task<IEnumerable<Claim>> GetClaimsFromUser(ApplicationUser user,ClaimsPrincipal subject)
{
var claims = new List<Claim>
{
new Claim(JwtClaimTypes.Subject, user.Id),
new Claim(JwtClaimTypes.PreferredUserName, user.UserName),
new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName)
};
if (!string.IsNullOrWhiteSpace(user.Name))
claims.Add(new Claim("name", user.Name));
if (!string.IsNullOrWhiteSpace(user.LastName))
claims.Add(new Claim("last_name", user.LastName));
claims.Add(new Claim(PermissionConstants.PackedPermissionClaimType,
await _calcAllowedPermissions.CalcPermissionsForUserAsync(user.Id)));
if (_userManager.SupportsUserEmail)
{
claims.AddRange(new[]
{
new Claim(JwtClaimTypes.Email, user.Email),
new Claim(JwtClaimTypes.EmailVerified, user.EmailConfirmed ? "true" : "false", ClaimValueTypes.Boolean)
});
}
if (_userManager.SupportsUserPhoneNumber && !string.IsNullOrWhiteSpace(user.PhoneNumber))
{
claims.AddRange(new[]
{
new Claim(JwtClaimTypes.PhoneNumber, user.PhoneNumber),
new Claim(JwtClaimTypes.PhoneNumberVerified, user.PhoneNumberConfirmed ? "true" : "false", ClaimValueTypes.Boolean)
});
}
return claims;
}
}
GetClaimsFromUser method is where you should add your claims.
The issue was I was not passing the Subject claim.
public static List<TestUser> GetSampleUsers()
{
var subjectId = Guid.NewGuid().ToString();
return new List<TestUser>
{
new TestUser
{
Username = "admin@local.com",
Password = "RockStar.1",
Claims = new List<Claim>
{
new Claim(JwtClaimTypes.Name, "Admin "),
new Claim(JwtClaimTypes.GivenName, "Admin"),
new Claim(JwtClaimTypes.FamilyName, "add min"),
new Claim(JwtClaimTypes.Email, "admin@local.com"),
new Claim(JwtClaimTypes.Subject, subjectId) --> This solve the issue
}
}
};
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.