ASP.NET
Core Identity Server 4 Authorization Code PKCE
JS
For how PKCE
Authorization Code Flow works, you can have a look on my previous article: [ASP.NET
Core] Identity Server 4 – PKCE Authorization Code Flow.
The flow is as
following, we will focus on how to create a JavaScript client that can
authenticate user by Identity Server 4 and access the backend’s resource with a
given Access Token.
Notice that I
put the JavaScript client at the Backend Server to make the sample code simple
(As the following figure).
In the real
world, we have to separate the Javascript client (Client) and the Backend
Server (Resource server), see reference: [ASP.NET
Core] Identity Server 4 – Concepts.
Here is the
final result’s demo.
▋Related articles
01.
[OpenLDAP] Create an OpenLDAP
container
02. [ASP.NET Core] Identity Server 4
– Concepts
03.
[ASP.NET Core] Identity Server 4
– LDAP authentication
04.
[ASP.NET Core] Identity Server 4
– Secure Web API
05.
[ASP.NET Core] Identity Server 4
– Custom Event Sink
06.
[ASP.NET Core] Identity Server 4
– Refresh Token
07.
[ASP.NET Core] Identity Server 4
– Role based authorization
08.
[ASP.NET Core] Identity Server 4
– Policy based authorization
09.
[ASP.NET Core] Identity Server 4
- Dockerize
10.
[ASP.NET Core] Identity Server 4
– Client Credential
11. [ASP.NET Core] Identity Server 4
– Policy based authorization with custom Authorization Handler
12.
[ASP.NET Core] Identity Server 4
– Signing credential
13.
[ASP.NET Core] Identity Server 4
– Authenticate by multiple LDAP
14.
[ASP.NET Core] Identity Server 4
– Cache and refresh Discovery document
15.
[ASP.NET Core] Identity Server 4 – PKCE Authorization Code Flow
16.
[ASP.NET Core] Identity Server 4 – PKCE Authorization Code Flow (Javascript client)
▋Docker 18.05.0-ce
▋.NET Core SDK 3.1.201
▋IdentityServer4 3.1.2
▋oidc-client.js 1.11.5
The source code
is on my Github.
▋PKCE Client configuration on Auth Server
Go to Idsrv4 project
(Auth Server), and open InMemoryInitConfig.cs to set the below
configuration on a new client:
▋InMemoryInitConfig.cs
Notice that there
are some key settings:
Configuration
|
Description
|
Value
|
AllowedGrantTypes
|
Grant
type
|
GrantTypes.Code
|
RequirePkce
|
Specifies
whether a proof key is required for authorization code based token requests
(defaults to false).
|
true
|
RequireClientSecret
|
If
set to false, no client secret is needed to request tokens at the token
endpoint.
|
false
|
RedirectUris
|
The
allowed Uri(s) to return Authorization Code or Tokens.
|
|
PostLogoutRedirectUris
|
Specifies
allowed URIs to redirect to after logout
|
|
AllowedCorsOrigins
|
Gets
or sets the allowed CORS origins for JavaScript clients.
|
|
public static IEnumerable<Client> GetClients ()
{
return new [] {
new Client {
ClientId = "PkceJS",
ClientName = "JavaScript Client",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
RedirectUris = { "https://localhost:5001/OpenId/Login/JS" },
PostLogoutRedirectUris = { "https://localhost:5001/OpenId/Login/JS" },
AllowedCorsOrigins = { "https://localhost:5001" },
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
ApiResources.MyBackendApi2
},
AllowOfflineAccess = true,
AccessTokenLifetime = 3600,
RefreshTokenUsage = TokenUsage.OneTimeOnly, // Or ReUse
RefreshTokenExpiration = TokenExpiration.Sliding,
AbsoluteRefreshTokenLifetime = 360000,
SlidingRefreshTokenLifetime = 36000,
ClientClaimsPrefix = string.Empty,
}
}
}
We have to implement
the AccountController in Identity Server, the code is as same as
[ASP.NET Core] Identity Server 4
– PKCE Authorization Code Flow
Or you can see the
full code on Github.
▋Client side
Since we put the
JavaScript client on Backend project, which is based on ASP.NET Core.
We have to do some initialization
works to enable related JS files.
First, install oidc-client.js with NPM.
$ cd src/AspNetCore.IdentityServer4.WebApi
$ npm install oidc-client --save
Copy node_modules/oidc-client/dist/oidc-client.js
(or oidc-client.min.js)
to wwwroot/js/.
And create the
following js files:
The js file
structure is as following for reference.
├── node_modules
| ├── oidc-client
| | ├── dist
| | | ├── oidc-client.d.ts
| | | ├── oidc-client.js
| | | ├── oidc-client.min.js
| | | ├── oidc-client.rsa256.slim.js
| | | ├── oidc-client.rsa256.slim.min.js
| | | ├── oidc-client.slim.js
| | | └── oidc-client.slim.min.js
└── wwwroot
├── js
| ├── app-config.js
| ├── app.js
| └── oidc-client.js
Before we implement
the client logic, we have to enable static file serving.
▋Startup.cs: Configure
public void Configure(IApplicationBuilder app)
{
// Use static files
app.UseStaticFiles();
}
▋Views/LoginByJs.cshtml
Let’s create a View
as the main page, which will import the js files we had just created.
<h2 id="welcome_msg">
Welcome! <label id="uid"></label>. <button class="btn btn-warning" id="logout">Sign Out</button>
<button class="btn btn-success" id="api">Test secured API</button>
</h2>
<h2 id="signin_msg">
Welcome! Please <button class="btn btn-primary" id="login">Sign In</button> first.
</h2>
<table class="table">
<thead class="thead-dark">
<tr>
<th>#</th>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">1</th>
<td>id_token</td>
<td><label id="id_token"></label></td>
</tr>
<tr>
<th scope="row">2</th>
<td>access_token</td>
<td><label id="access_token"></label></td>
</tr>
<tr>
<th scope="row">3</th>
<td>refresh_token</td>
<td><label id="refresh_token"></label></td>
</tr>
<tr>
<th scope="row">4</th>
<td>expires_at</td>
<td><label id="expires_at"></label></td>
</tr>
</tbody>
</table>
<hr />
<h3>Result:</h3>
<pre id="results"></pre>
@section Scripts {
<script src="~/js/oidc-client.js"></script>
<script type="module" src="~/js/app-config.js"></script>
<script type="module" src="~/js/app.js"></script>
}
The page will be
like following, there are three buttons that we will implement their callbacks.
· Sign In
· Sign Out
· Test secured API
(Before logged in)
(Logged in)
▋app-config.js
We will set the
client and identity server’s host URLs that will be used for redirect URLs
after logged in or out in app.js.
export const AUTH_HOST_URL = "https://localhost:6001";
export const CLIENT_HOST_URL = "https://localhost:5001";
▋app.js
Set the OIDC
configuration and logging,
import * as constants from './app-config.js';
function log() {
document.getElementById('results').innerText = '';
Array.prototype.forEach.call(arguments, function (msg) {
if (msg instanceof Error) {
msg = "Error: " + msg.message;
}
else if (typeof msg !== 'string') {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById('results').innerHTML += msg + '\r\n';
});
}
var config = {
authority: constants.AUTH_HOST_URL,
client_id: "PkceJS",
redirect_uri: `${constants.CLIENT_HOST_URL}/OpenId/Login/JS`,
response_type: "code",
scope: "openid profile offline_access MyBackendApi2",
post_logout_redirect_uri: `${constants.CLIENT_HOST_URL}/OpenId/Login/JS`
};
Implement the
callbacks of 【Sign In】, 【Sign Out】 and 【Test secured API】.
document.getElementById("welcome_msg").hidden = true;
document.getElementById("login").addEventListener("click", login, false);
document.getElementById("api").addEventListener("click", api, false);
document.getElementById("logout").addEventListener("click", logout, false);
var mgr = new Oidc.UserManager(config);
mgr.signinRedirectCallback().then(function (user) {
if (user) {
document.getElementById("signin_msg").hidden = true;
document.getElementById("welcome_msg").hidden = false;
document.getElementById("uid").innerText = user.profile.sub;
document.getElementById("id_token").innerText = user.id_token;
document.getElementById("access_token").innerText = user.access_token;
document.getElementById("refresh_token").innerText = user.refresh_token;
document.getElementById("expires_at").innerText = moment.unix(user.expires_at).utc();
log("User logged in", user);
}
else {
log("User not logged in");
}
});
function login() {
mgr.signinRedirect();
}
function logout() {
// Signout
mgr.signoutRedirect();
}
function api() {
mgr.getUser().then(function (user) {
var url = `${constants.CLIENT_HOST_URL}/api/DemoPolicyBased/Admin/Get`; // You can change the test API
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = function () {
log(xhr.status, xhr.responseText);
}
xhr.setRequestHeader("Authorization", "Bearer " + user.access_token);
xhr.send();
});
}
The full code of app.js
is here.
▋Source Code
Github: KarateJB/AspNetCore.IdentityServer4.Sample
▋Authorization Code Flow with Proof Key for Code
Exchange (PKCE)
▋IdentityModel/oidc-client-js
▋IdentityServer4: Adding a JavaScript client