Identity Server: Interactive Login using MVC

This post is a continuation of a series of posts that follow my initial looking into using IdentityServer4 in ASP.NET Core with an API and an Angular front end. The following are the related posts.

Identity Server: Introduction
Identity Server: Sample Exploration and Initial Project Setup
Identity Server: Interactive Login using MVC (this post)
Identity Server: From Implicit to Hybrid Flow
Identity Server: Using ASP.NET Core Identity
Identity Server: Using Entity Framework Core for Configuration Data
Identity Server: Usage from Angular

As before the end goal will be having authorization happen from Angular, but in the short term, the Client Application is using MVC/Razor for testing and verifications. The code as it stood before this post can be found here. If you are following along with the official docs I wrote this post while working through the Adding User Authentication with OpenID Connect quickstart.

The main point of this post is to add a way for a user to enter their username and password and get access to a page that requires authorization using the OpenID Connect protocol.

Identity Application

To enable this scenario the Identity Application will need MVC added along with some UI that will be used to handle login, permissions, and log off. First, using NuGet install the following two packages.

  • Microsoft.AspNetCore.Mvc
  • Microsoft.AspNetCore.StaticFiles

Next, in the ConfigureServices of the Startup class MVC needs to be added as a service.

services.AddMvc();

Then in the Configure function use static files and use MVC should be added after the use statement for IdentityServer.

app.UseIdentityServer();
app.UseDeveloperExceptionPage();

// Following are adds
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
UI Changes

For the type of flow being used in this sample, the Identity Application will be in control of the login, grant, log out, and related UI. This is not a small amount of thing to get set up properly. Thankfully the IdentityServer team provides a Quickstart UI for use with the in-memory items we are currently using. The files can be downloaded from the repo linked in the previous line or an easier way is to open a Powershell prompt in the same directory of the Identity application as the Startup.cs file and run the following command.

iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/release/get.ps1'))

After the download the project will contain a Quickstart folder with the needed controllers, a Views with of course the needed views, and wwwroot will have all the related files that need to be served with the views.

Config Changes

The Config class needs to be changed to return some more in-memory information to make this new process work. The first is to add a new client for MVC to the GetClients function. The following is the full function, but it is the second Client is the new one.

public static IEnumerable<Client> GetClients()
{
    return new List<Client>
    {
        new Client
        {
            ClientId = "clientApp",
            AllowedGrantTypes = GrantTypes.ClientCredentials,

            // secret for authentication
            ClientSecrets =
            {
                new Secret("secret".Sha256())
            },

            // scopes that client has access to
            AllowedScopes = { "apiApp" }
        },

        // OpenID Connect implicit flow client (MVC)
        new Client
        {
            ClientId = "mvc",
            ClientName = "MVC Client",
            AllowedGrantTypes = GrantTypes.Implicit,

            RedirectUris = { "http://localhost:5002/signin-oidc" },
            PostLogoutRedirectUris = 
                { "http://localhost:5002/signout-callback-oidc" },

            AllowedScopes =
            {
                IdentityServerConstants.StandardScopes.OpenId,
                IdentityServerConstants.StandardScopes.Profile
            }
        }
    };
}

Notice that for the OpenID Connect implicit flow there are URLs that are needed that so this flow knows how to call back into the client application. At this point, I haven’t dug into everything that is going on in the client. The ClientId, ClientName, and URLs related properties are pretty clear. I am not 100% on the AllowedGrantTypes and AllowedScopes, but at this point, I am not going to dive into on these two options.

Next, add a GetIdentityResources function matching the following. This fall in the same category as the two properties above, we are using them without fully digging into them.

public static IEnumerable<IdentityResource> GetIdentityResources()
{
    return new List<IdentityResource>
    {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
    };
}

The last change to the Config class is to add a function to return the in-memory users.

public static List<TestUser> GetUsers()
{
    return new List<TestUser>
    {
        new TestUser
        {
            SubjectId = "1",
            Username = "alice",
            Password = "password",

            Claims = new List<Claim>
            {
                new Claim("name", "Alice"),
                new Claim("website", "https://alice.com")
            }
        },
        new TestUser
        {
            SubjectId = "2",
            Username = "bob",
            Password = "password",

            Claims = new List<Claim>
            {
                new Claim("name", "Bob"),
                new Claim("website", "https://bob.com")
            }
        }
    };
}
Startup Changes

The last change in the Identity Application is to add the new in-memory items to the IdentityServer service in the ConfigureServices function. The following is the full function.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddIdentityServer()
        .AddTemporarySigningCredential()
        .AddInMemoryIdentityResources(Config.GetIdentityResources())
        .AddInMemoryApiResources(Config.GetApiResources())
        .AddInMemoryClients(Config.GetClients())
        .AddTestUsers(Config.GetUsers());
}

Client Application

In order to get the client application to play well with the changes in the Identity Application, a few changes need to be made. First, the following NuGet packages need to be installed.

  • Microsoft.AspNetCore.Authentication.Cookies
  • Microsoft.AspNetCore.Authentication.OpenIdConnect

Next, in the Configure function of the Startup class, the application’s middleware pipeline needs some changes. Add the following line to turn off the JWT claim type mapping. This must be done before calling UseOpenIdConnectAuthentication.

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

Now add in the cookie authentication middleware.

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationScheme = "Cookies"
});

The last change is to add OpenID Connect authentication to the pipeline placed after the cookies middleware.

app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
    AuthenticationScheme = "oidc",
    SignInScheme = "Cookies",

    Authority = "http://localhost:5000",
    RequireHttpsMetadata = false,

    ClientId = "mvc",
    SaveTokens = true
});

Notice that the URL of the authority is the URL the Identity Application runs on as well as the client ID match the one we set up in the GetClients function of the Config class in the Identity Application.

Identity Controller

Now that the above is set up we can switch over to the IdentityController and add the Authorize attribute to the Index function.

[Authorize]
public async Task<IActionResult> Index()

This means if a user hits the index action of this controller and isn’t logged in they will be presented with the login page and after login, they will be redirected back to the above index action. That whole process is handled by the OpenId Connect Authentication middleware. The first time I tested the flow and it just worked was magical.

 

The final set of changes for this post is going to be added a way to log out. In the IdentityController add a Logout function.

public async Task Logout()
{
    await HttpContext.Authentication.SignOutAsync("Cookies");
    await HttpContext.Authentication.SignOutAsync("oidc");
}
Identity View Changes

The last change is to add a logout button to the Index.cshtml found in the Views/Identity directory. At the bottom of the page, the following was added to call the Logout action.

<form asp-controller="Identity" asp-action="Logout" method="post">
    <button type="submit">Logout</button>
</form>

Wrapping Up

I already liked the idea of IdentityServer before this post, but after playing with it with the changes above it is emphasized how nice it is. I am very happy I am going down this path instead of trying to work this all out on my own. Stay tuned as this exploration will continue in future posts.

The code in the finished state can be found here.

Update

Turns out there is a bug in the code that goes with this example. In the client application’s IdentityConroller the call to get a token is using clientApp instead of mvc for the client ID when requesting a token. With that change, the call to the API Application will fail since the MVC client doesn’t have access to the API scope. Look for next weeks post where API access will be added to the MVC client.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.