Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET / ASP.NET-Core

Getting Started with IdentityServer4 - Quick Reference

5.00/5 (2 votes)
8 Oct 2019CPOL3 min read 14.4K  
These notes are based on my experience getting started with IdentityServer4.

Introduction

I recently decided to add authorization and authentication to my suite of training modules. I selected IdentityServer4 as the tool to use and based my effort on the 'combined' example published by the IdentityServer4 team using EntityFramework published on Github.

I could not find a handy reference card to state the minimum setting changes that it should work with. This document is produced from my notes with the aim of closing that gap.

Using IdentityServer4

I choose not to write my own identity server, opting instead to extend the one on the official 'combined' example listed above. In this section, I set out what you need to do to each component so that an MVC client and an API whose authentication is managed by the identity server may communicate with one or more API's.

Identity Server

Your identity server implementation must include entries on the following tables:

  • [Client] table (pay attention to id and ClientID) - These include all MVC, JavaScript and console clients of your server - but not APIs called. Note for many of the tables that follow, it is the value of the ID column here that will feature in the clientID column of that table, e.g., [ClientGrantTypes], [ClientPostLogoutRedirectUris]
  • [ApiResources] - These identify the APIs that your clients will call- when incorrect, may result in "Sorry there was an error: Unauthorized_Client"
  • [ApiScopes] - These identify the APIs that your clients will call- when incorrect, may result in "Sorry there was an error: Invalid_Scope". Some redundancy in what is recorded on this and the ApiResources table. The join to ApiResources is on the ApiResourceId Column.
  • [ClientCorsOrigins] Needs the URL of your identity server for CORS protection on JavaScript clients.
  • [ClientSecrets] - secrets that your server will expect from its clients ID maps to ID on the Client table. It is encrypted, and for now, I am using the secret from the Identity Server examples. Leaving the Expiration null means your token will never expire.
  • [ClientScopes] Entries on this table are used to determine what APIs your client will have access to.
  • [ClientGrantTypes] - Manages how your client interacts with the token service. See http://docs.identityserver.io/en/latest/topics/grant_types.html
  • [ClientRedirectUris] At a minimum here, you need an entry to yourUrl/signin-oidc, e.g., https://localhost:6001/signin-oidc and the ID of your client. As your system grows, you will need to add more entries here.
  • [ClientPostLogoutRedirectUris] An entry here is used to form a clean link back to your calling client e.g., https://localhost:6001/signout-callback-oidc
  • You may also add a certificate in the startup.cs of your Identity Server, e.g., .AddSigningCredential("CN=IdentityServerCN"), but this is not mandatory. That said, if you code for it, then it will have to be there.

Meanwhile in config.cs (following the published example):

C#
    public static IEnumerable<apiresource> GetApis()
    {
        return new List<apiresource>
        {
            new ApiResource("api1", "My API")
        };
    }

    public static IEnumerable<client> GetClients()
    {
        return new List<client>
        {
            new Client
            {
                ClientId = "client",

                // no interactive user, use the clientid/secret for authentication
                AllowedGrantTypes = GrantTypes.ClientCredentials,

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

                // scopes that client has access to
                AllowedScopes = { "api1" }
            },
            // resource owner password grant client
            new Client
            {
                ClientId = "ro.client",
                AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

                ClientSecrets =
                {
                    new Secret("secret".Sha256())
                },
                AllowedScopes = { "api1" }
            },
            new Client
            {
                ClientId = "WebPub",
                ClientName = "Vendatic Public Website",
                AllowedGrantTypes = GrantTypes.Hybrid,

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

                RedirectUris           =
                { "http://localhost:5002/signin-oidc" }, //URL of the example MVC client
                PostLogoutRedirectUris =
                { "http://localhost:5002/signout-callback-oidc" },//URL of the
                                                                  //example MVC client

                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    "api1"
                },

                AllowOfflineAccess = true
            },
            // JavaScript Client
            new Client
            {
                ClientId = "js",
                ClientName = "JavaScript Client",
                AllowedGrantTypes = GrantTypes.Code,
                RequirePkce = true,
                RequireClientSecret = false,

                RedirectUris =           { "http://localhost:5003/callback.html" },
                PostLogoutRedirectUris = { "http://localhost:5003/index.html" },
                AllowedCorsOrigins =     { "http://localhost:5003" },

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

Shown with hardcodes for brevity.

MVC Client

Here is a code snippet from the startup.cs of my client (ASP.NET Core 2.2):

C#
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

services.AddAuthentication(options =>
{
    options.DefaultScheme = this.configMaster.p_DefaultScheme;
    options.DefaultChallengeScheme = this.configMaster.p_DefaultChallengeScheme;
})
    .AddCookie(this.configMaster.p_DefaultScheme)
    .AddOpenIdConnect(this.configMaster.p_DefaultChallengeScheme, options =>
    {
        options.SignInScheme = this.configMaster.p_DefaultScheme;
        options.Authority = this.configMaster.p_Authority;
        options.RequireHttpsMetadata = this.configMaster.p_RequireHttpsMetadata;
        options.ClientId = this.configMaster.p_ClientId;
        options.ClientSecret = IdentityServerClientSecret;
        options.ResponseType = this.configMaster.p_ResponseType;
        options.SaveTokens = this.configMaster.p_SaveTokensCoerced;
        options.GetClaimsFromUserInfoEndpoint = 
            this.configMaster.p_GetClaimsFromUserInfoEndpointCoerced;
            
        foreach (CScopeMaster scope in this.scopes)
        {
            options.Scope.Add(scope.p_AddScopeFor);
        }
        
        options.ClaimActions.MapJsonKey
          (this.configMaster.p_ClaimType, this.configMaster.p_ActionsJsonKey);
          
        // Redirect to home on authorization rejected
        options.Events = new OpenIdConnectEvents
        {
            OnRemoteFailure = (context) =>
            {
                context.Response.Redirect("/");
                context.HandleResponse();
                
                return Task.CompletedTask;
            }
        };
    });

I have most I need coming in from my configuration master table, with the scopes coming from their own table (without a scope entry here, an API will not accept your token later) and the secret is coming from the .NET user secret store. It comes after AddMvc in ConfigureServices.

Here is the corresponding code from the published IdentityServer sample, 8_AspNetIdentity, for comparison.

C#
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

services.AddAuthentication(options =>
    {
        options.DefaultScheme = "Cookies";
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie("Cookies")
    .AddOpenIdConnect("oidc", options =>
    {
        options.SignInScheme = "Cookies";

        options.Authority = "http://localhost:5000";
        options.RequireHttpsMetadata = false;

        options.ClientId = "mvc";
        options.ClientSecret = "secret";
        options.ResponseType = "code id_token";

        options.SaveTokens = true;
        options.GetClaimsFromUserInfoEndpoint = true;

        options.Scope.Add("api1");
        options.Scope.Add("offline_access");

        options.ClaimActions.MapJsonKey("website", "website");
    });

API Client

Here is how any API needs to be set up. Again, it's in ConfigureServices of startup.cs after AddMvc:

C#
services.AddMvcCore()
    .AddAuthorization()
    .AddJsonFormatters();

services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = "https://localhost:5101";
        options.RequireHttpsMetadata = false;

        options.Audience = "api1";
    });

services.AddCors(options =>
{
    // this defines a CORS policy called "default"
    options.AddPolicy("default", policy =>
    {
        policy.WithOrigins("http://localhost:5003") //Could be another client
                                                    //- see combined example.
            .AllowAnyHeader()
            .AllowAnyMethod();
    });
});

Further Reading

History

  • 9th October, 2019: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)