This tutorial walks you through the necessary steps to integrate IdentityServer in a JS application. Since all the steps will be done on the client side, we’ll use a JS library, oidc-client-js, to help with tasks like obtaining and validating tokens.
You can find the code associated with this walkthrough here.
The walkthrough is split in 3 parts:
- Authenticate against IdentityServer in the JS application
- Make API calls from the JS application
- Have a look at how to renew tokens, log out and check sessions
Part 1 - Authentication against IdentityServer
This first part will focus on allowing us to authenticate in the JS application. To do so, we will create 2 projects; one for the JS application and one for IdentityServer.
Create the JS application project
In Visual Studio, create an empty web application.
Note the URL assigned to the project:
Create the IdentityServer project
In Visual Studio, create another empty web application for IdentityServer.
You can switch the project now to SSL using the properties window:
Important Don’t forget to update the start URL in your project properties so that it reflects the HTTPS url of the project.
Adding IdentityServer
IdentityServer is based on OWIN/Katana and distributed as a NuGet package. To add it to the newly created web host, install the following two packages:
Install-Package Microsoft.Owin.Host.SystemWeb -ProjectName IdentityServer
Install-Package IdentityServer3 -ProjectName IdentityServer
Configuring IdentityServer - Clients
IdentityServer needs some information about the clients it is going to support, this can be easily achieved by supplying a collection of Client
objects:
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
Enabled = true,
ClientName = "JS Client",
ClientId = "js",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"http://localhost:56668/popup.html"
},
AllowedCorsOrigins = new List<string>
{
"http://localhost:56668"
},
AllowAccessToAllScopes = true
}
};
}
}
A special setting here is the AllowedCorsOrigins
property. This allows IdentityServer to only accept browser-based requests from registered URLs.
More on the popup.html
later on.
Remark Right now the client has access to all scopes (via the AllowAccessToAllScopes
setting). For production applications you would narrow that down to only the scopes it’s expected to access with the AllowedScopes
property.
Configuring IdentityServer - Users
Next we will add some users to IdentityServer - again this can be accomplished by providing a simple C# class. You can retrieve user information from any data store and we provide out of the box support for ASP.NET Identity and MembershipReboot.
public static class Users
{
public static List<InMemoryUser> Get()
{
return new List<InMemoryUser>
{
new InMemoryUser
{
Username = "bob",
Password = "secret",
Subject = "1",
Claims = new[]
{
new Claim(Constants.ClaimTypes.GivenName, "Bob"),
new Claim(Constants.ClaimTypes.FamilyName, "Smith"),
new Claim(Constants.ClaimTypes.Email, "bob.smith@email.com")
}
}
};
}
}
Configuring IdentityServer - Scopes
Finally we will add scopes to IdentityServer. For authentication we’ll only put standard OIDC scopes. When we’ll integrate API calls we’ll create our own.
public static class Scopes
{
public static List<Scope> Get()
{
return new List<Scope>
{
StandardScopes.OpenId,
StandardScopes.Profile
};
}
}
Adding Startup
IdentityServer is configured in the startup class. Here we provide information about the clients, users, scopes,
the signing certificate and some other configuration options.
In production you should load the signing certificate from the Windows certificate store or some other secured source.
In this sample we simply added it to the project as a file (you can download a test certificate from here.
Add it to the project and set its Copy to Output Directory
property to Copy always
.
For info on how to load the certificate from Azure WebSites see here.
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = LoadCertificate(),
Factory = new IdentityServerServiceFactory()
.UseInMemoryUsers(Users.Get())
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get())
});
}
private static X509Certificate2 LoadCertificate()
{
return new X509Certificate2(
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"bin\Config\idsrv3test.pfx"), "idsrv3test");
}
}
At this point you have a fully functional IdentityServer and you can browse to the discovery endpoint to inspect the configuration:
RAMMFAR
One last thing, please don’t forget to add RAMMFAR to your web.config, otherwise some of our embedded assets will not be loaded correctly by IIS:
<system.webServer>
<modules runAllManagedModulesForAllRequests="true" />
</system.webServer>
JS Client - setup
We use several third-party libraries to support our application:
We are going to install them with npm, the Node.js front-end package manager. If you don’t have npm installed, you can follow these instructions on the npm website.
Once npm is installed, open a command-line prompt in the JsApplication
folder:
$ npm install jquery
$ npm install bootstrap
$ npm install oidc-client
By default, npm installs packages in the node_modules
folder.
Important npm packages are usually not committed to source control. If you cloned the repository containing the final source code and want to restore the npm packages, open a
command-line prompt in the JsApplication
folder and run npm install
to restore packages.
We also create a basic index.html
file:
<!DOCTYPE html>
<html>
<head>
<title>JS Application</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.css" />
<style>
.main-container {
padding-top: 70px;
}
pre:empty {
display: none;
}
</style>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">JS Application</a>
</div>
</div>
</nav>
<div class="container main-container">
<div class="row">
<div class="col-xs-12">
<ul class="list-inline list-unstyled requests">
<li><a href="index.html" class="btn btn-primary">Home</a></li>
<li><button type="button" class="btn btn-default js-login">Login</button></li>
</ul>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="panel panel-default">
<div class="panel-heading">ID Token Contents</div>
<div class="panel-body">
<pre class="js-id-token"></pre>
</div>
</div>
</div>
</div>
</div>
<script src="node_modules/jquery/dist/jquery.js"></script>
<script src="node_modules/bootstrap/dist/js/bootstrap.js"></script>
<script src="node_modules/oidc-client/dist/oidc-client.js"></script>
</body>
</html>
and a popup.html
file:
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8" />
</head>
<body>
<script src="node_modules/oidc-client/dist/oidc-client.js"></script>
</body>
</html>
We have two HTML files because oidc-client
can open a popup to show the login form to the user.
JS Client - authentication
Now that we have everything we need, we can configure our login settings in index.html
thanks to the UserManager
JS class.
// helper function to show data to the user
function display(selector, data) {
if (data && typeof data === 'string') {
data = JSON.parse(data);
}
if (data) {
data = JSON.stringify(data, null, 2);
}
$(selector).text(data);
}
var settings = {
authority: 'https://localhost:44300',
client_id: 'js',
popup_redirect_uri: 'http://localhost:56668/popup.html',
response_type: 'id_token',
scope: 'openid profile',
filterProtocolClaims: true
};
var manager = new Oidc.UserManager(settings);
var user;
manager.events.addUserLoaded(function (loadedUser) {
user = loadedUser;
display('.js-user', user);
});
$('.js-login').on('click', function () {
manager
.signinPopup()
.catch(function (error) {
console.error('error while logging in through the popup', error);
});
});
Let’s go quickly through the settings:
authority
is the base URL of our IdentityServer instance. This will allowoidc-client
to query the metadata endpoint so it can validate the tokensclient_id
is the id of the client we want to use when hitting the authorization endpointpopup_redirect_uri
is the redirect URL used when using thesigninPopup
method. If you prefer not having a popup and redirecting the user in the main window, you can use theredirect_uri
property and thesigninRedirect
methodresponse_type
defines in our case that we only expect an identity token backscope
defines the scopes the application asks forfilterProtocolClaims
indicates to oidc-client if it has to filter some OIDC protocol claims from the response:nonce
,at_hash
,iat
,nbf
,exp
,aud
,iss
andidp
We also handle clicks on the Login button to open the login page popup. The signinPopup
returns a Promise
which is resolved when the user data has been retrieved and validated.
This data is accessible via 2 ways:
- as the resolved value of the underlying Promise
- as the data associated with the
userLoaded
event
In our case, we added a handler to the userLoaded
event by passing a callback function to the events.addUserLoaded
method.
The data contains several properties like id_token
, scope
and profile
that all contain different pieces of data.
We also have to configure popup.html
:
new Oidc.UserManager().signinPopupCallback();
Under the hoods, the instance of UserManager
in the index.html
page opens a popup and redirects it to the login page. When IdentityServer redirects the user to the popup page, the information is then passed back to the main page and the popup is automatically closed.
At this stage, you can login:
You can try and set the filterProtocolClaims
property to false
and see the additional claims being stored in the profile
property.
JS application - scopes
Remember we configured our user with an email
claim? It doesn’t show up in the identity token because the scopes the client asked for - openid
and profile
- don’t contain this claim.
If you want to get the user’s email, you’ll have to ask for another scope which is called email
by editing the scopes
property of the UserManager
settings.
In our case, the only modification we need to make is to let IdentityServer know that this scope exists in the Scopes
class:
public static class Scopes
{
public static List<Scope> Get()
{
return new List<Scope>
{
StandardScopes.OpenId,
StandardScopes.Profile,
// New scope
StandardScopes.Email
};
}
}
We don’t have to change anything in the client configuration because we specified it has access to all the scopes. In a realistic scenario, you want to give a client access to only the scopes it’s expected to request, so this would require a change in the client configuration as well.
After this, we can see the email
claim in the user profile:
Part 2 - API call
In the second part, we’ll see how you can call a protected API from the JS application. We will need to get, along with the identity token, an access token from IdentityServer when we login and use it when calling the API.
Create the API project
Create a new empty web application in Visual Studio.
This time the URL that has to be assigned to the project is http://localhost:60136
.
Configuring the API
For this example, we’ll create a very simple API based on ASP.NET Web API. To do so, install the following packages:
Install-Package Microsoft.Owin.Host.SystemWeb -ProjectName Api
Install-Package Microsoft.Owin.Cors -ProjectName Api
Install-Package Microsoft.AspNet.WebApi.Owin -ProjectName Api
Install-Package IdentityServer3.AccessTokenValidation -ProjectName Api
Important The IdentityServer3.AccessTokenValidation
package has an indirect dependency on System.IdentityModel.Tokens.Jwt
.
At the time of writing, globally updating Api
project NuGet packages brings down version 5.0.0
of System.IdentityModel.Tokens.Jwt
which causes an error when starting the Api
project:
The solution is to bring back an older compatible version of System.IdentityModel.Tokens.Jwt
Install-Package System.IdentityModel.Tokens.Jwt -ProjectName Api -Version 4.0.2.206221351
Let’s now create a Startup
class and build our OWIN/Katana pipeline.
public class Startup
{
public void Configuration(IAppBuilder app)
{
// Allow all origins
app.UseCors(CorsOptions.AllowAll);
// Wire token validation
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44300",
// For access to the introspection endpoint
ClientId = "api",
ClientSecret = "api-secret",
RequiredScopes = new[] { "api" }
});
// Wire Web API
var httpConfiguration = new HttpConfiguration();
httpConfiguration.MapHttpAttributeRoutes();
httpConfiguration.Filters.Add(new AuthorizeAttribute());
app.UseWebApi(httpConfiguration);
}
}
This is all straight-forward, but let’s have a closer look at what we use in our pipeline:
Since it is the JS application which will make the calls to the API, CORS must be enabled. In our case, we allow all origins to access it. Once again, in a real scenario, we would lock this down to allow only the expected origins.
We then use token validation provided by the IdentityServer3.AccessTokenValidation
package. By setting the Authority
property, the metadata document will be retrieved and used to configure the token validation settings.
Since version 2.2, IdentityServer implements the introspection endpoint to validate tokens. This endpoint requires scope authentication which makes it more secured than the traditional access token validation endpoint.
Finally, we add our Web API configuration. Note we use a global AuthorizeAttribute
which makes every endpoint of the API only accessible to authenticated requests.
Let’s now add a basic endpoint to our API:
[Route("values")]
public class ValuesController : ApiController
{
private static readonly Random _random = new Random();
public IEnumerable<string> Get()
{
var random = new Random();
return new[]
{
_random.Next(0, 10).ToString(),
_random.Next(0, 10).ToString()
};
}
}
Updating identityServer configuration
We introduced a new api
scope which we have to register in IdentityServer. This is done by editing the Scopes
class of the identityServer
project:
public static class Scopes
{
public static List<Scope> Get()
{
return new List<Scope>
{
StandardScopes.OpenId,
StandardScopes.Profile,
StandardScopes.Email,
// New scope registration
new Scope
{
Name = "api",
DisplayName = "Access to API",
Description = "This will grant you access to the API",
ScopeSecrets = new List<Secret>
{
new Secret("api-secret".Sha256())
},
Type = ScopeType.Resource
}
};
}
}
The new scope is a resource scope which means it will end up in the access token. Once again, we don’t need to allow the client to request this new scope in this example because of the special setting, but it will be a necessary step in a real scenario.
Updating the JS application
We can now update the JS application settings so it will request the new api
scope when logging in the user.
var settings = {
authority: 'https://localhost:44300',
client_id: 'js',
popup_redirect_uri: 'http://localhost:56668/popup.html',
// We add `token` to specify we expect an access token too
response_type: 'id_token token',
// We add the new `api` scope to the list of requested scopes
scope: 'openid profile email api',
filterProtocolClaims: true
};
The modifications include:
- a new panel to show the access token
- an updated
response_type
to specify we want an access token back along with the identity token - the new
api
scope to be requested as part of the login request
The access token is exposed via the access_token
property and its expiration via the expires_at
property.
It is worth noting that oidc-client
takes away a lot of pain by taking care of validating the tokens with the signing certificate, we don’t have to write code.
After logging in, here’s what we get:
Calling the API
Now that we have an access token, we can include the call to the API:
[...]
<div class="container main-container">
<div class="row">
<div class="col-xs-12">
<ul class="list-inline list-unstyled requests">
<li><a href="index.html" class="btn btn-primary">Home</a></li>
<li><button type="button" class="btn btn-default js-login">Login</button></li>
<!-- New button to trigger an API call -->
<li><button type="button" class="btn btn-default js-call-api">Call API</button></li>
</ul>
</div>
</div>
<div class="row">
<!-- Make the existing sections 6-column wide -->
<div class="col-xs-6">
<div class="panel panel-default">
<div class="panel-heading">User data</div>
<div class="panel-body">
<pre class="js-user"></pre>
</div>
</div>
</div>
<!-- And add a new one for the result of the API call -->
<div class="col-xs-6">
<div class="panel panel-default">
<div class="panel-heading">API call result</div>
<div class="panel-body">
<pre class="js-api-result"></pre>
</div>
</div>
</div>
</div>
</div>
[...]
$('.js-call-api').on('click', function () {
var headers = {};
if (user && user.access_token) {
headers['Authorization'] = 'Bearer ' + user.access_token;
}
$.ajax({
url: 'http://localhost:60136/values',
method: 'GET',
dataType: 'json',
headers: headers
}).then(function (data) {
display('.js-api-result', data);
}).catch(function (error) {
display('.js-api-result', {
status: error.status,
statusText: error.statusText,
response: error.responseJSON
});
});
});
We now have a button which will trigger the API call along with another panel to show the call response.
Please note that the access token is passed in the Authorization
header of the request.
Here’s what we see if we call the API prior to login:
And after login:
In the first case, there was no access token, hence no Authorization
header in the request, so the access token validation middleware did nothing. The request flowed through the API as unauthenticated, the global AuthorizeAttribute
rejected it and responded with a 401 Unauthorized
error.
In the second case, the token validation middleware found the token in the Authorization
header, passed it along to the introspection endpoint which flagged it as valid, and an identity was created with the claims it contained. The request, this time authenticated, flowed to Web API, the AuthorizeAttribute
contrainsts were satisfied, and the endpoint was invoked.
Part 3 - Renewing tokens, logging out and checking sessions
We now have a working JS application which logs in against IdentityServer and makes successful calls to a protected API.
But users will soon encounter issues when their access token expires and is rejected by the access token validation middleware on the API.
To work around that, we can setup oidc-token-manager
to automatically renew the access token when it’s about to expire, without any steps required for the user.
Expired tokens
Let’s first see how we can have a token expire on purpose. We have to reduce the lifetime of the access token.
This is a per-client setting, so we’ll have to edit our Clients
class in the IdentityServer project:
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
Enabled = true,
ClientName = "JS Client",
ClientId = "js",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"http://localhost:56668/popup.html"
},
AllowedCorsOrigins = new List<string>
{
"http://localhost:56668"
},
AllowAccessToAllScopes = true,
AccessTokenLifetime = 10
}
};
}
}
The access token lifetime, which is 1 hour by default, has been changed to 10 seconds.
What you’ll experience if you login the JS application again is that you’ll get the same 401 Unauthorized
error when you call the API 10 seconds after logging in.
Renewing tokens
We are going to rely on a feature oidc-client-js
gives us to renew the tokens.
Internally, the JS library keeps track of the expiration time of the access token and can request a new one by issuing a new authorization request to IdentityServer.
This will be invisible to the user as the prompt
setting, which will be set to none
, prevents the user from having to log in or give his consent while he has a valid session.
IdentityServer will return a new access token which will replace the one that is about to expire.
There are several settings related to access token expiration and renewal:
- The
accessTokenExpiring
event will be fired when the access token is about to expire - The
accessTokenExpiringNotificationTime
can be used to tweak how far before the token expires theaccessTokenExpiring
event is fired. The default value is60
seconds - Another setting is named
automaticSilentRenew
which instructs the library to automatically renew the access token when it’s about to expire - Finally, the
silent_redirect_uri
setting needs to be configured so the library can specify it as a return URL when trying to get a new token
Here is how oidc-client-js
handles automatic token renewal.
When the token is about to expire, a dynamic hidden iframe
will be created.
In this iframe
a new authorization request will be made to IdentityServer. If the request succeeds, identityServer will redirect the iframe
to the specified silent redirect URL, in which a piece of JS code will update the user information so that the main window gets access to it/
Let’s make some modifications in our configuration to take advantage of these capabilities.
var settings = {
authority: 'https://localhost:44300',
client_id: 'js',
popup_redirect_uri: 'http://localhost:56668/popup.html',
// Add the slient renew redirect URL
silent_redirect_uri: 'http://localhost:56668/silent-renew.html'
response_type: 'id_token token',
scope: 'openid profile email api',
// Add expiration nofitication time
accessTokenExpiringNotificationTime: 4,
// Setup to renew token access automatically
automaticSilentRenew: true,
filterProtocolClaims: true
};
Since we specified a new page as the silent_redirect_uri
, we have to create that page
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8" />
</head>
<body>
<script src="node_modules/oidc-client/dist/oidc-client.js"></script>
<script>
new Oidc.UserManager().signinSilentCallback();
</script>
</body>
</html>
The second step is to let IdentityServer know that it is OK to redirect the user to that new page after the authentication is successful:
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
Enabled = true,
ClientName = "JS Client",
ClientId = "js",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"http://localhost:56668/popup.html",
// The new page is a valid redirect page after login
"http://localhost:56668/silent-renew.html"
},
AllowedCorsOrigins = new List<string>
{
"http://localhost:56668"
},
AllowAccessToAllScopes = true,
AccessTokenLifetime = 70
}
};
}
}
When the renewal is successful, the UserManager
will raise a userLoaded
event.
Since we already handle this event, the updated data will automatically be captured and displayed on the UI.
When it fails, it will raise a silentRenewError
event, which we can subscribe to so we know when something went wrong
manager.events.addSilentRenewError(function (error) {
console.error('error while renewing the access token', error);
});
We updated the access token lifetime to 10 seconds and instructed oidc-client-js
to renew the token 4 seconds before it expires.
So now, after logging in, we can see that every 6 seconds we get a fresh access token from IdentityServer.
Logging out
Logging out of a JS application has a different meaning than from a server-side application, because if you refresh the main page, you will lose the tokens and will have to login again. But when the login popup opens, it could be that you still have a valid session cookie for the IdentityServer web application. It could then be possible that the popup doesn’t prompt you for your credentials and close itself. This is similar to when the token manager silently renews the token.
Logging out here means logging out of IdentityServer so that, next time you try to login from an IdentityServer-protected application, you will have to enter your credentials again.
The process here is simple, we just need a logout button that calls the signoutRedirect
method of the UserManager
instance. We also need to let IdentityServer know that the specified post-logout redirect URL is valid:
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
Enabled = true,
ClientName = "JS Client",
ClientId = "js",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"http://localhost:56668/popup.html",
"http://localhost:56668/silent-renew.html"
},
// Valid URLs after logging out
PostLogoutRedirectUris = new List<string>
{
"http://localhost:56668/index.html"
},
AllowedCorsOrigins = new List<string>
{
"http://localhost:56668"
},
AllowAccessToAllScopes = true,
AccessTokenLifetime = 70
}
};
}
[...]
<div class="row">
<div class="col-xs-12">
<ul class="list-inline list-unstyled requests">
<li><a href="index.html" class="btn btn-primary">Home</a></li>
<li><button type="button" class="btn btn-default js-login">Login</button></li>
<li><button type="button" class="btn btn-default js-call-api">Call API</button></li>
<!-- New logout button -->
<li><button type="button" class="btn btn-danger js-logout">Logout</button></li>
</ul>
</div>
</div>
var settings = {
authority: 'https://localhost:44300',
client_id: 'js',
popup_redirect_uri: 'http://localhost:56668/popup.html',
silent_redirect_uri: 'http://localhost:56668/silent-renew.html',
// Add the post logout redirect URL
post_logout_redirect_uri: 'http://localhost:56668/index.html',
response_type: 'id_token token',
scope: 'openid profile email api',
accessTokenExpiringNotificationTime: 4,
automaticSilentRenew: true,
filterProtocolClaims: true
};
[...]
$('.js-logout').on('click', function () {
manager
.signoutRedirect()
.catch(function (error) {
console.error('error while signing out user', error);
});
});
When clicking the Logout
button, the user will be redirected to IdentityServer so that the session cookie is cleared.
Please note the screenshot above shows a page served by IdentityServer, not the JS application
While this example shows how to logout the user via the main window, it’s worth noting that oidc-client-js
also provides a way to make this happen in a popup, much like the login was implemented.
You’ll find more information in the documentation of oidc-client-js
.
Check session
The session in our JS application starts when the identity token we get back from IdentityServer is validated.
IdentityServer itself supports session management so it returns, in the authorization response, a value named session_state
.
You can find the OpenID Connect spec related to that matter here.
In some cases, we might be interested to know if the user ended their session on IdentityServer, for example by logging out of another application which in turned logged them out of IdentityServer.
The way to do this this is to compute the session_state
value. If it’s equal to the one IdentityServer sent, this means the session state is unchanged, so the user is still logged in. It it’s different, something changed, possibly the user logged out. In this case it’s advised to issue a silent authorization request, with prompt=none
. If it succeeds, we get a new identity token, and it means the session on the IdentityServer side is still valid. If it fails, the user has logged out, and we have to ask them to log in again.
Unfortunately, the JS application on its own cannot compute the session_state
value because it depends on the IdentityServer session cookie value which it doesn’t have access to.
The design of the specification requires to load, in a hidden iframe
, the checksession endpoint from IdentityServer. The JS application and this iframe
can then communicate with the postMessage
API.
The checksession endpoint
This endpoint serves a simple page which listens to messages sent with postMessage
. The data passed in the message is used to compute the session state hash. If it matches the one sent by IdentityServer, the page sends a message back to the calling window with the value unchanged
. It it doesn’t, it sends back changed
. If something goes wrong, it sends error
.
Building the session check feature
Fortunately, oidc-client-js
takes care of everything.
As a matter of fact, the default settings monitor the session state already.
The name of the associated property is monitorSession
.
This means that right after the user is logged in, oidc-client-js
creates a hidden iframe
in which the sessioncheck endpoint from IdentityServer is loaded.
At regular intervals, a message is sent to that iframe
with both the client id and the session state.
Messages sent to the iframe
are also handled and the received value determines if a session change has happened.
To acknowledge this works as expected, we’ll take advantage of the logging system provided by oidc-client-js
.
By default, a no-op logger is used, but we can have the library log messages to the browser console.
Oidc.Log.logger = console;
To minimise the amount of logged messages, we’ll also increase the access token lifetime. Lots of messages are logged when renewing tokens, and with the current settings this happens every 6 seconds. Let’s increase the lifetime to 1 minute.
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
Enabled = true,
ClientName = "JS Client",
ClientId = "js",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"http://localhost:56668/popup.html",
"http://localhost:56668/silent-renew.html"
},
PostLogoutRedirectUris = new List<string>
{
"http://localhost:56668/index.html"
},
AllowedCorsOrigins = new List<string>
{
"http://localhost:56668"
},
AllowAccessToAllScopes = true,
// Access token lifetime increased to 1 minute
AccessTokenLifetime = 60
}
};
}
Lastly, when a session change is detected and an automatic signin doesn’t succeed, the UserManager
raises a userSignedOut
event.
Let’s add a handler to this event.
manager.events.addUserSignedOut(function () {
alert('The user has signed out');
});
After navigating back to the application, logging out, opening the console, and logging back in, we can see in the console that every 2 seconds - the default interval - oidc-client-js
checks for us
that the session at IdentityServer is still valid
To prove that it works, let’s open a second tab, navigate to the JS application and login.
Now both tabs check the state of the session with IdentityServer.
Logout from one of the tab and observe the userSignedOut
being handled and the popup appear.