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):
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",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1" }
},
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" },
PostLogoutRedirectUris =
{ "http://localhost:5002/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
},
AllowOfflineAccess = true
},
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);
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 =>
{
options.AddPolicy("default", policy =>
{
policy.WithOrigins("http://localhost:5003")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
Further Reading
History
- 9th October, 2019: Initial version