fbpx

04. PART 1 IdentityServer4 ASP.NET Core Identity .NET Core 3.1

You can find the project here.

What is ASP.NET Core Identity

The official explanation from Microsoft docs is: “ASP.NET Core Identity is a membership system that adds login functionality to ASP.NET Core apps. Users can create an account with the login information stored in Identity or they can use an external login provider.” and “Identity can be configured using a SQL Server database to store user names, passwords, and profile data.”.

So it is a membership system that takes care of members (another word for users) and it is used by ASP.NET Core apps (what we are building). Users can create local accounts stored in Identity (another name for user store) or can use any external provider like Google, Okta, Microsoft, Facebook, etc. User data can be persisted to a standard SQL database (which we already have). ASP.NET Identity standardizes user store with structure (tables) and methods to manipulate the store. For example to create a new user account, just invoke a method. It will validate the data and store it in a database. To log in, just invoke a login method, it will validate the password (for a local login) and return a valid response.

To set or change the password, just invoke a method and the password will get hashed and stored in the database. So a user store is a set of business logic contained in the methods that operate on data storage. The data structure of user tables is standardized so one day when ASP.NET Core is updated it will be easy to reuse existing tables or run a migration to another storage if needed.

This is opposite to a custom user store where the data structure is custom so manual migration must be performed. Sometimes migration is not even possible because the password hashing algorithm used in the old and new store is different and there is no way to extract the plain text password from hashes and re-hash it. Business logic contained in methods for the ASP.NET Identity user store is tested, security standards are validated and are pretty much stable and secure as you can get it. It doesn’t cover all scenarios but it’s really easy to extend it as shown in one of my next tutorials where we will add a custom property to a user.

 

Why do we even need ASP.NET Core Identity?

We need it because IdentityServer4 doesn’t care about the users. It handles token generation, token endpoints, discovery endpoint, OAuth2 and OIDC protocols, clients, scopes, all the important bits except for the users. In order to get our Identity Server to start caring about the users (local and external), we should provide it with a user store.

ASP.NET Identity is a good match as it’s a mature system for user management that is used by all ASP.NET applications, .net core and .net framework. You can use ASP.NET Core Identity without IdentityServer4 to authenticate single application but you lose the ability to create an Identity Provider (IdP) which is a whole point of these tutorials to have an SSO provider for all apps and not redo the auth for each app individually.

 

Code changes in order to implement ASP.NET Core Identity

Note: I will continue on the previous “02. IdentityServer4 EntityFramework” tutorial using the MSSQL server. For good people following the PostgreSQL tutorial all the steps that will be laid out in this tutorial are pretty much the same except you will use “UseNpgsql” instead of “UseSqlServer” extension method.

Note 2: When we start doing scaffolding some CSS styles are going to be overwritten that will make IdentityServer4 look a bit weird and out of place. I suggest keeping a copy of “wwwroot/css” folder so you can overwrite the changes later on. Specific files that we need are “site.less”, “site.css” and “site.min.css”.

 

Let’s start by adding a NuGet package for IdentityServer4 ASP.NET Core Identity support. The package name is “IdentityServer4.AspNetIdentity”.

 

Now we will add the ApplicationUser class that will inherit from the IdentityUser class. The reason for this is to be able to extend IdentityUser in the future (add additional properties to the user entity). In future tutorials, I will add an example of how to add custom user properties. For now, we will just create a new folder called “Models” and add “ApplicationUser.cs” file to hold the class definition like so

using Microsoft.AspNetCore.Identity;

namespace IdentityServer.Models
{
	public class ApplicationUser : IdentityUser
	{
	
	}
}

 

The next step will scaffold MVC controllers and views for ASP.NET Identity. This step is not mandatory as the same functionality can be obtained by just using the NuGet package for ASP.NET Core Identity (“Microsoft.AspNetCore.Identity”). The difference is that scaffolding these resources in our project directly will allow for easy modification of the look and feel of ASP.NET Core Identity.

 

In Solution Explorer right-click on “Identity Server” project → Add → New Scaffolded Item

 

The “Add Scaffold” dialog should pop-up, select “Identity” and click “Add”

 

The “Add Identity” dialog will pop-up. Select an existing Layout.cshtml file, in our project the location is “~/Views/Shared/_Layout.cshtml”. Click on “Override all files” checkbox to select all files and than uncheck “Account\ConfirmEmailChange” (have error and will not scafold item with that). We need to create a new data context class, let’s call it “IdentityServer.Models.IdentityDbContext”. We will also specify the user class, so let’s use the “ApplicationUser” we added in the previous step.

 

Wait a bit for Visual Studio to do its magic and you should see a new folder called “Areas” containing all juicy ASP.NET Core views and controllers for us to play with.

 

Let’s just reorganize a bit for easier maintenance in the future. Move (drag and drop) “IdentityDbContext.cs” file from “Areas/Identity/Data” into the “Data” folder in the project root. You can now delete the “Areas/Identity/Data” folder.

We will also simplify lambda expression in “Areas/Identity/IdentityHostingStartup.cs” as we will move that configuration into our project’s Startup in the next step. Better to keep all config as visible as possible and in one place. So open up “Areas/Identity/IdentityHostingStartup.cs” and leave only the Configure method like so

using Microsoft.AspNetCore.Hosting;
 
[assembly: HostingStartup(typeof(IdentityServer.Areas.Identity.IdentityHostingStartup))]

namespace IdentityServer.Areas.Identity
{
	public class IdentityHostingStartup : IHostingStartup
	{
		public void Configure(IWebHostBuilder builder)
		{
			builder.ConfigureServices((context, services) => {
			
			});
		}
	}
}

 

Last thing to remove is “Quickstart/TestUsers.cs” file that contains TestUsers class that initializes dummy users. We don’t need it anymore as when we finish all code changes the users will come from user store. Feel free to delete the “Quickstart/TestUsers.cs” file now.

Refactoring time. Open “Startup.cs” to add missing bits for ASP.NET Core Identity.

Add missing “using” directives

using IdentityServer.Models;
using Microsoft.AspNetCore.Identity;

 

Add code snippet for IdentityDbContext and ASP.NET Core Identity service (add it above the IdentityServer service configuration, above the “AddIdentityServer” extension method)

services.AddDbContext<IdentityDbContext>(options =>
	options.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly))
);

services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
	options.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<IdentityDbContext>()
.AddDefaultTokenProviders();

 

Last thing to add in Startup is to add ASP.NET Core Identity to IdentityService service configuration (after the “AddOperationStore”)

.AddAspNetIdentity<ApplicationUser>();

 

This is the complete ConfigureServices method body with ASP.NET Core Identity changes from above

public void ConfigureServices(IServiceCollection services)
{
    string connectionString = Configuration.GetConnectionString("DefaultConnection");
    var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

    // uncomment, if you want to add an MVC-based UI
    services.AddControllersWithViews();

    services.AddDbContext<IdentityDbContext>(options => options.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)));
    services.AddIdentity<ApplicationUser, IdentityRole>(options =>
    {
        options.SignIn.RequireConfirmedEmail = true;
    })
        .AddEntityFrameworkStores<IdentityDbContext>()
        .AddDefaultTokenProviders();

    var builder = services.AddIdentityServer(options =>
    {
        options.Events.RaiseErrorEvents = true;
        options.Events.RaiseInformationEvents = true;
        options.Events.RaiseFailureEvents = true;
        options.Events.RaiseSuccessEvents = true;
        options.UserInteraction.LoginUrl = "/Account/Login";
        options.UserInteraction.LogoutUrl = "/Account/Logout";
        options.Authentication = new AuthenticationOptions()
        {
            CookieLifetime = TimeSpan.FromHours(10), // ID server cookie timeout set to 10 hours
            CookieSlidingExpiration = true
        };
    })
    .AddConfigurationStore(options =>
    {
        options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
    })
    .AddOperationalStore(options =>
    {
        options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
        options.EnableTokenCleanup = true;
    })
    .AddAspNetIdentity<ApplicationUser>();

    // not recommended for production - you need to store your key material somewhere secure
    builder.AddDeveloperSigningCredential();
}

 

If you try to build the solution now you will get two errors one in “AccountController” and another in “ExternalController”. This is because we removed the “TestUsers” class and we need to update local login and external login controllers to use the ASP.NET Core Identity user store.

“Quickstart/Account/AccountController.cs” changes

Remove obsolete “using” directive

using IdentityServer4.Test;

 

Add missing “using” directives

using IdentityServer.Models;
using Microsoft.AspNetCore.Identity;

 

Remove obsolete variable

private readonly TestUserStore _users;

 

Add private variables to hold injected references of UserManager and SignInManager

private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;

 

Replace the constructor with code below

public AccountController(
	UserManager<ApplicationUser> userManager,
	SignInManager<ApplicationUser> signInManager,
	IIdentityServerInteractionService interaction,
	IClientStore clientStore,
	IAuthenticationSchemeProvider schemeProvider,
	IEventService events)
{
	_userManager = userManager;
	_signInManager = signInManager;
	_interaction = interaction;
	_clientStore = clientStore;
	_schemeProvider = schemeProvider;
	_events = events;
}

 

Replace second “Login” method with code below. This change will remove code that used the “TestUsers” class and use UserManager and SignInManager instead.

public async Task<IActionResult> Login(LoginInputModel model, string button)
{
    // check if we are in the context of an authorization request
    var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);

    // the user clicked the "cancel" button
    if (button != "login")
    {
        if (context != null)
        {
            // if the user cancels, send a result back into IdentityServer as if they 
            // denied the consent (even if this client does not require consent).
            // this will send back an access denied OIDC error response to the client.
            await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);

            // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-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 });
            }

            return Redirect(model.ReturnUrl);
        }
        else
        {
            // since we don't have a valid context, then we just go back to the home page
            return Redirect("~/");
        }
    }

    if (ModelState.IsValid)
    {
        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.UserName));

            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"));
        ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
    }

    // something went wrong, show form with error
    var vm = await BuildLoginViewModelAsync(model);
    return View(vm);
}

 

Replace the second “Logout” method with code below. This change will use SignInManager for logout.

public async Task<IActionResult> Logout(LogoutInputModel model)
{
    // build a model so the logged out page knows what to display
    var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);

    if (User?.Identity.IsAuthenticated == true)
    {
        // delete local authentication cookie
        await _signInManager.SignOutAsync();

        // raise the logout event
        await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
    }

    // check if we need to trigger sign-out at an upstream identity provider
    if (vm.TriggerExternalSignout)
    {
        // build a return URL so the upstream provider will redirect back
        // to us after the user has logged out. this allows us to then
        // complete our single sign-out processing.
        string url = Url.Action("Logout", new { logoutId = vm.LogoutId });

        // this triggers a redirect to the external provider for sign-out
        return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
    }

    return View("LoggedOut", vm);
}

 

Last change in AccountController is to replace the “BuildLoginViewModelAsync” with the code below.

private async Task<LoginViewModel> BuildLoginViewModelAsync(string returnUrl)
{
	var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
	if (context?.IdP != null)
	{
		// this is meant to short circuit the UI and only trigger the one external IdP
		return new LoginViewModel
		{
			EnableLocalLogin = false,
			ReturnUrl = returnUrl,
			Username = context?.LoginHint,
			ExternalProviders = new ExternalProvider[] { new ExternalProvider { AuthenticationScheme = context.IdP } }
		};
	}

	var schemes = await _schemeProvider.GetAllSchemesAsync();

	var providers = schemes
		.Where(x => x.DisplayName != null ||
					(x.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase))
		)
		.Select(x => new ExternalProvider
		{
			DisplayName = x.DisplayName,
			AuthenticationScheme = x.Name
		}).ToList();

	var allowLocal = true;
	if (context?.ClientId != null)
	{
		var client = await _clientStore.FindEnabledClientByIdAsync(context.ClientId);
		if (client != null)
		{
			allowLocal = client.EnableLocalLogin;

			if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any())
			{
				providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList();
			}
		}
	}

	return new LoginViewModel
	{
		AllowRememberLogin = AccountOptions.AllowRememberLogin,
		EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin,
		ReturnUrl = returnUrl,
		Username = context?.LoginHint,
		ExternalProviders = providers.ToArray()
	};
}

 

AccountController is done. This will take care of the local login to use the ASP.NET Core Identity user store. Let’s work on the ExternalController now. An external controller handles external logins (from Okta, Google, Azure AD, etc.)

“Quickstart/Account/ExternalController.cs” changes

Remove obsolete “using” directive

using IdentityServer4.Test;

 

Add missing “using” directives

using IdentityServer.Models;
using Microsoft.AspNetCore.Identity;

 

Remove obsolete variable

private readonly TestUserStore _users;

 

Add private variables to hold injected references of UserManager and SignInManager

private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;

 

Replace the constructor with code below

public ExternalController(
	UserManager<ApplicationUser> userManager,
	SignInManager<ApplicationUser> signInManager,
	IIdentityServerInteractionService interaction,
	IClientStore clientStore,
	IEventService events)
{
	_userManager = userManager;
	_signInManager = signInManager;
	_interaction = interaction;
	_clientStore = clientStore;
	_events = events;
}

 

Replace the “Callback” method with code below. This will help us split up functionality across several methods.

public async Task<IActionResult> Callback()
{
	// read external identity from the temporary cookie
	var result = await HttpContext.AuthenticateAsync(IdentityConstants.ExternalScheme);
	if (result?.Succeeded != true)
	{
		throw new Exception("External authentication error");
	}

	// lookup our user and external provider info
	var (user, provider, providerUserId, claims) = await FindUserFromExternalProviderAsync(result);
	if (user == null)
	{
		// this might be where you might initiate a custom workflow for user registration
		// in this sample we don't show how that would be done, as our sample implementation
		// simply auto-provisions new external user
		user = await AutoProvisionUserAsync(provider, providerUserId, claims);
	}

	// this allows us to collect any additonal claims or properties
	// for the specific prtotocols used and store them in the local auth cookie.
	// this is typically used to store data needed for signout from those protocols.
	var additionalLocalClaims = new List<Claim>();
	var localSignInProps = new AuthenticationProperties();
	ProcessLoginCallbackForOidc(result, additionalLocalClaims, localSignInProps);
	ProcessLoginCallbackForWsFed(result, additionalLocalClaims, localSignInProps);
	ProcessLoginCallbackForSaml2p(result, additionalLocalClaims, localSignInProps);

	// issue authentication cookie for user
	// we must issue the cookie maually, and can't use the SignInManager because
	// it doesn't expose an API to issue additional claims from the login workflow
	var principal = await _signInManager.CreateUserPrincipalAsync(user);
	additionalLocalClaims.AddRange(principal.Claims);
	var name = principal.FindFirst(JwtClaimTypes.Name)?.Value ?? user.Id;
	await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.Id, name));
	await HttpContext.SignInAsync(user.Id, name, provider, localSignInProps, additionalLocalClaims.ToArray());

	// delete temporary cookie used during external authentication
	await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);

	// validate return URL and redirect back to authorization endpoint or a local page
	var returnUrl = result.Properties.Items["returnUrl"];
	if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl))
	{
		return Redirect(returnUrl);
	}

	return Redirect("~/");
}

 

Replace the “FindUserFromExternalProviderAsync” method with code below. It will add couple more ways to match local user with the external user using different claims like name, email, preferred name.

private async Task<(ApplicationUser user, string provider, string providerUserId, IEnumerable<Claim> claims)>
	FindUserFromExternalProviderAsync(AuthenticateResult result)
{
	var externalUser = result.Principal;

	// try to determine the unique id of the external user (issued by the provider)
	// the most common claim type for that are the sub claim and the NameIdentifier
	// depending on the external provider, some other claim type might be used
	var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ??
					  externalUser.FindFirst(ClaimTypes.NameIdentifier) ??
					  throw new Exception("Unknown userid");

	// remove the user id claim so we don't include it as an extra claim if/when we provision the user
	var claims = externalUser.Claims.ToList();
	claims.Remove(userIdClaim);

	var provider = result.Properties.Items["scheme"];
	var providerUserId = userIdClaim.Value;

	// find external user
	var user = await _userManager.FindByLoginAsync(provider, providerUserId);

	// try to find user by name and/or email
	if (user == null)
	{
		var name = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Name)?.Value ?? claims.FirstOrDefault(x => x.Type == ClaimTypes.Name)?.Value;
		if (name != null)
		{
			user = await _userManager.FindByNameAsync(name);
		}
		if (user == null)
		{
			var prefname = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.PreferredUserName)?.Value;
			if (prefname != null)
			{
				user = await _userManager.FindByNameAsync(prefname);
			}
		}
		if (user == null)
		{
			var email = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Email)?.Value ?? claims.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value;
			if (email != null)
			{
				user = await _userManager.FindByEmailAsync(email);
			}
		}
		if (user != null)
		{
			var identityResult = await _userManager.AddLoginAsync(user, new UserLoginInfo(provider, providerUserId, provider));
			if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description);
		}
	}

	return (user, provider, providerUserId, claims);
}

 

Finally, the last change is to replace the “AutoProvisionUser” method with the code below. This method is in charge of creating a new local user for the external user login if the local user couldn’t be found in the local user store.

private async Task<ApplicationUser> AutoProvisionUserAsync(string provider, string providerUserId, IEnumerable<Claim> claims)
{
	// create a list of claims that we want to transfer into our store
	var filtered = new List<Claim>();

	// user's display name
	var name = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Name)?.Value ??
		claims.FirstOrDefault(x => x.Type == ClaimTypes.Name)?.Value;
	if (name != null)
	{
		filtered.Add(new Claim(JwtClaimTypes.Name, name));
	}
	else
	{
		var first = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.GivenName)?.Value ??
			claims.FirstOrDefault(x => x.Type == ClaimTypes.GivenName)?.Value;
		var last = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.FamilyName)?.Value ??
			claims.FirstOrDefault(x => x.Type == ClaimTypes.Surname)?.Value;
		if (first != null && last != null)
		{
			filtered.Add(new Claim(JwtClaimTypes.Name, first + " " + last));
		}
		else if (first != null)
		{
			filtered.Add(new Claim(JwtClaimTypes.Name, first));
		}
		else if (last != null)
		{
			filtered.Add(new Claim(JwtClaimTypes.Name, last));
		}
	}

	// email
	var email = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Email)?.Value ??
	   claims.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value;
	if (email != null)
	{
		filtered.Add(new Claim(JwtClaimTypes.Email, email));
	}

	var user = new ApplicationUser
	{
		UserName = Guid.NewGuid().ToString(),
	};
	var identityResult = await _userManager.CreateAsync(user);
	if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description);

	if (filtered.Any())
	{
		identityResult = await _userManager.AddClaimsAsync(user, filtered);
		if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description);
	}

	identityResult = await _userManager.AddLoginAsync(user, new UserLoginInfo(provider, providerUserId, provider));
	if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description);

	return user;
}

 

Phew. That was quite a load of work to do. ExternalController is done. The project should be able to build now as we removed all references to the “TestUsers” class.

 

More details

Let’s recap. So we removed the “TestUsers” class that contained hardcoded users. We added NuGet packages for ASP.NET Core Identity. We scaffolded views and controllers for ASP.NET Core Identity that will allow us to modify them in the future. They are responsible for a password reset, 2FA, user registration, etc so it’s a good thing to have them in solution ready for any modification needed. One important thing to notice is that we will not be using Login and Logout controller/view from ASP.NET Core Identity because we will use those provided by IdentityServer4. We actually specified this in Startup

 

options.UserInteraction.LoginUrl = "/Account/Login";
options.UserInteraction.LogoutUrl = "/Account/Logout";

 

After we scaffolded the ASP.NET Core Identity views and controllers we modified the AccountController and ExternalController for local login and external login to use ASP.NET Core instead of the hardcoded “TestUsers”. We added the service configuration needed for ASP.NET Core Identity in Startup so it will use the SQL server to store the users. In one of the future tutorials, I will show you how to extend the “ApplicationUser” and add custom properties to the user.

If you kept a copy of “wwwroot/css” folder you can now overwrite the files “site.less”, “site.css” and “site.min.css” with your backup copy to get them to the previous version. This will make IdentityServer4 look good (you might notice some weird header issues after scaffolding).

This was a PART 1 of ASP.NET Core Identity tutorial. In PART 2 we will create database migrations, run the migrations to create the database tables and explain each table, similar as we did for the IdentityServer4 but this time for ASP.NET Core Identity.

You can find the project here.

Support

For direct assistance schedule a technical meeting with Ivan to talk about your requirements. For a general overview of our services and a live demo schedule a meeting with Maja.

Comments
  • Katie says:

    After following the tutorial I had a few things I still needed to do, in order for it to compile. Thought I’d share in case anyone else did too.
    – I had to add “Using Microsoft.AspNetCore.Identity” to RegisterConfirmation.cshtml.cs so that it would recognize the UserManager object.
    – I had to comment out “onException($”Unexpected error occurred removing external login for user with ID ‘{userId}’.”);” on or near line 63 in the OnPostRemoveLoginAsync method in ExternalLogs.cshtml.cs. It seemed to just be random code in there…not sure where it came from.

    Thank you so much for these tutorials!

  • Jan says:

    I had to change in the callback method (ExternalController.cs) the constant:
    var result = await HttpContext.AuthenticateAsync(IdentityConstants.ExternalScheme);
    to
    var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);

  • marina says:

    I get these errors :
    1>Areas\Identity\Pages\Account\RegisterConfirmation.cshtml.cs(15,26,15,54): error CS0246: Le nom de type ou d’espace de noms ‘UserManager’ est introuvable (vous manque-t-il une directive using ou une référence d’assembly ?)
    1>Areas\Identity\Pages\Account\RegisterConfirmation.cshtml.cs(18,42,18,70): error CS0246: Le nom de type ou d’espace de noms ‘UserManager’ est introuvable (vous manque-t-il une directive using ou une référence d’assembly ?)
    how can I resolve it please?

  • SeanR says:

    Great article series – thank you.
    Prior to net core 3.1 and IdentityServer4 I would use an Int for the PK of my Application User like:
    AppUser – public class MyUser : IdentityUser
    DBContext – public class MyContext : IdentityDbContext
    If I try for an Int PK when using IdentityServer4
    DBContext – public class MyContext : ApiAuthorizationDbContext
    I get an error ‘Using the generic type ‘ApiAuthorizationDbContext’ requires 1 type arguments’. Does this mean I cannot have a different type of PK for my App user class?
    Thanks

  • Mario Levesque says:

    Minor, but just thought I would mention. Saw these two errors so un-commented the stubs at the bottom of the ExternalController.cs file to fix it.
    The name ‘ProcessLoginCallbackForWsFed’ does not exist in the current context
    The name ‘ProcessLoginCallbackForSaml2p’ does not exist in the current context

  • Duncan Spence says:

    I have a problem with Account\ExternalController.cs under latest version of IdentityServer (4.0.1)
    Several compile errors, most are easy to fix with a bit of investigation, but the method signature for HttpContext.SignInAsync has changed too far for me to resolve and there are no changes in GitHub that I can see: –

    await HttpContext.SignInAsync(user.Id, name, provider, localSignInProperties, additionalLocalClaims.ToArray());

    No overload for method ‘SignInAsync’ takes 5 arguments

    • deblokt says:

      Please check the official IdentityServer4 tutorials about how to utilize the latest version.

      • Sargis says:

        So far I’ve gotten past the SignInAsync issue, by changing that call to the following:
        await _signInManager.SignInWithClaimsAsync(user, localSignInProps, additionalLocalClaims);

        I can’t guarantee that this is 100% working as the previous call did yet, since I don’t have clients/users utilizing the external flow. It does at least get past the build error.

  • SreeRam says:

    Looks like there are breaking changes in core 3.1 causing errors in AccountController and ExternalController.
    AccountController.cs
    1. Login(LoginInputModel model, string button) -> “await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);” is no longer valid. ConsentResponse does not contain a definition for ‘Denied’. I changed the code like this “await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);”
    2. In the same Login method “var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl)” is returning AuthorizationRequest and it does not contain ClientId property. Now it has Client object as a property. As a result wherever context.ClientId is used must be replaced with context.Client.ClientId
    ExternalController.cs
    1. In Callback() method (line 115), “HttpContext.SignInAsync(user.Id, name, provider, localSignInProps, additionalLocalClaims.ToArray());” showing error “No overload for method ‘SignInAsync’ takes 5 arguments”. Solution not yet known. if anyone knows, please paste the solution. Otherwise will investigate and post the solution later.

  • Hardik Pancholi says:

    I am getting few errors.
    1. ExternalController : line AccountOptions.WindowsAuthenticationSchemeName –> AccountOptions doesn’t contain definition for WindowsAuthenticationSchemeName
    2. ExternalController : line AccountOptions.IncludeWindowsGroups —> Same as point 1.
    3. ExetrnalController : line await HttpContext.SignInAsync(user.Id, name, provider, localSignInProps, additionalLocalClaims.ToArray()) –>No overload method “SignInAsync” takes 5 arguments.
    4. AccountController : line context?.ClientId != null –> AuthorizationRequest doesn’t contain definition for ClientId
    5. AccontController : line ConsentResponse.Denied –> ConsentResponse doesn’t contain definition for Denied
    6. AccountContoller : line await _clientStore.IsPkceClientAsync(context.ClientId) –> IClientStore doesn’t contain definition for IsPkceClientAsync

    I am using latest version 4.0.2 of identityServer 4 and 3.1.5 version of Microsoft.aspNetcore.Identity

  • Brad says:

    This is a fantastic series! Thank-you very much for producing it!

    I’m sure this is likely something small I’m doing wrong (or more likely using the wrong NuGet package versions), but I get the following after making the changes to the AccountController:
    1>src\IdentityServer\Quickstart\Account\AccountController.cs(92,83,92,89): error CS0117: ‘ConsentResponse’ does not contain a definition for ‘Denied’
    1>src\IdentityServer\Quickstart\Account\AccountController.cs(95,70,95,78): error CS1061: ‘AuthorizationRequest’ does not contain a definition for ‘ClientId’ and no accessible extension method ‘ClientId’ accepting a first argument of type ‘AuthorizationRequest’ could be found (are you missing a using directive or an assembly reference?)
    1>src\IdentityServer\Quickstart\Account\AccountController.cs(95,44,95,61): error CS1061: ‘IClientStore’ does not contain a definition for ‘IsPkceClientAsync’ and no accessible extension method ‘IsPkceClientAsync’ accepting a first argument of type ‘IClientStore’ could be found (are you missing a using directive or an assembly reference?)
    1>src\IdentityServer\Quickstart\Account\AccountController.cs(121,74,121,82): error CS1061: ‘AuthorizationRequest’ does not contain a definition for ‘ClientId’ and no accessible extension method ‘ClientId’ accepting a first argument of type ‘AuthorizationRequest’ could be found (are you missing a using directive or an assembly reference?)
    1>src\IdentityServer\Quickstart\Account\AccountController.cs(121,48,121,65): error CS1061: ‘IClientStore’ does not contain a definition for ‘IsPkceClientAsync’ and no accessible extension method ‘IsPkceClientAsync’ accepting a first argument of type ‘IClientStore’ could be found (are you missing a using directive or an assembly reference?)
    1>src\IdentityServer\Quickstart\Account\AccountController.cs(239,59,239,90): error CS0117: ‘AccountOptions’ does not contain a definition for ‘WindowsAuthenticationSchemeName’
    1>src\IdentityServer\Quickstart\Account\AccountController.cs(248,25,248,34): error CS1061: ‘AuthorizationRequest’ does not contain a definition for ‘ClientId’ and no accessible extension method ‘ClientId’ accepting a first argument of type ‘AuthorizationRequest’ could be found (are you missing a using directive or an assembly reference?)
    1>src\IdentityServer\Quickstart\Account\AccountController.cs(250,84,250,92): error CS1061: ‘AuthorizationRequest’ does not contain a definition for ‘ClientId’ and no accessible extension method ‘ClientId’ accepting a first argument of type ‘AuthorizationRequest’ could be found (are you missing a using directive or an assembly reference?)

    I’ll keep working at it, but if this is a known problem, I wouldn’t mind some hints. 🙂

    Thanks!
    Brad.

    • Brad says:

      For ExternalController I get the following:
      1>src\IdentityServer\Quickstart\Account\ExternalController.cs(103,13,103,40): error CS0103: The name ‘ProcessLoginCallbackForOidc’ does not exist in the current context
      1>src\IdentityServer\Quickstart\Account\ExternalController.cs(104,13,104,41): error CS0103: The name ‘ProcessLoginCallbackForWsFed’ does not exist in the current context
      1>src\IdentityServer\Quickstart\Account\ExternalController.cs(105,13,105,42): error CS0103: The name ‘ProcessLoginCallbackForSaml2p’ does not exist in the current context
      1>src\IdentityServer\Quickstart\Account\ExternalController.cs(114,31,114,42): error CS1501: No overload for method ‘SignInAsync’ takes 5 arguments

    • Brad says:

      Yes, this appears to be due to the Quickstart having moved from 3.x to 4.x. I haven’t done tons of investigation yet, but I have believe I’ve confirmed that much, anyway.

Comments are closed.