Click here to Skip to main content
14,330,776 members

Getting Started with IdentityServer4 - Quick Reference

Rate this:
5.00 (1 vote)
Please Sign up or sign in to vote.
5.00 (1 vote)
8 Oct 2019CPOL
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.

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.
  • [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):

    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):

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.

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:

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)

Share

About the Author

Ger Hayden
Software Developer
Ireland Ireland
My first program was written in Basic on a Sinclair Spectrum ZX 16K in the summer of '85. Having studied Computer Systems I attempted to break into the world of C but took a wrong turn and got immersed in COBOL!

I looked a C again in 1994 but didnt follow up on it. In 2001 I introduced myself to Visual C++ 6.0 courtesy of Ivor Hortons book, but found the going difficult. I tipped my toe in the .NET water in '05 but the first example I tried in VC++ 2005 express didnt work and allied with the absence of MFC in the express package, I parked that up.

Along the way my career got shunted into software testing

A personal machine change force me to migrate to VS2008 in 2008. The new edition of Ivor Hortons book for VC++ in VS2008 reintroduced me to .NET and I got curious whereupon I went out and acquired Stephen Fraser's "Pro Visual C++/CLI and
the .NET 3.5 Platform". I was hooked!

After 20 years I think I finally found my destination.

But it would take a further 8 years of exile before I was reappointed to a developer role. In that time I migrated to C# and used selenium wedriver (courtesy of Arun Motoori's Selenium By Arun) as the catalyst to finally grab the opportunity.

Comments and Discussions

 
QuestionWould be great if you could post end to end example Pin
Sacha Barber10-Oct-19 3:58
mvaSacha Barber10-Oct-19 3:58 
AnswerRe: Would be great if you could post end to end example Pin
Ger Hayden10-Oct-19 8:25
memberGer Hayden10-Oct-19 8:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Tip/Trick
Posted 8 Oct 2019

Stats

1.6K views
3 bookmarked