This article presents some sample basic setup for OpenId Connect setup for Angular 8, targeting the npm library openid-client and Thinktecture IdentityServer 4 as the STS.
The following AutService sets up the UserManager first on the client side.
I must mention here that the setup ended up quite similar to the setup I did for an ASP.NET Core backend API I did for a React frontend SPA.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { UserManager, User, WebStorageStateStore, Log } from 'oidc-client';
import { Constants } from '../constants';
import { Utils } from './utils';
import { AuthContext } from '../model/auth-context';
@Injectable()
export class AuthService {
private _userManager: UserManager;
private _user: User;
authContext: AuthContext;
constructor(private httpClient: HttpClient) {
Log.logger = console;
const config = {
authority: Constants.stsAuthority,
client_id: Constants.clientId,
redirect_uri: `${Constants.clientRoot}assets/oidc-login-redirect.html`,
scope: 'openid projects-api profile',
response_type: 'id_token token',
post_logout_redirect_uri: `${Constants.clientRoot}?postLogout=true`,
userStore: new WebStorageStateStore({ store: window.localStorage }),
automaticSilentRenew: true,
silent_redirect_uri: `${Constants.clientRoot}assets/silent-redirect.html`
};
this._userManager = new UserManager(config);
this._userManager.getUser().then(user => {
if (user && !user.expired) {
this._user = user;
this.loadSecurityContext();
}
});
this._userManager.events.addUserLoaded(args => {
this._userManager.getUser().then(user => {
this._user = user;
this.loadSecurityContext();
});
});
}
login(): Promise<any> {
console.log('Inside the new auth service!');
return this._userManager.signinRedirect();
}
logout(): Promise<any> {
return this._userManager.signoutRedirect();
}
isLoggedIn(): boolean {
return this._user && this._user.access_token && !this._user.expired;
}
getAccessToken(): string {
return this._user ? this._user.access_token : '';
}
signoutRedirectCallback(): Promise<any> {
return this._userManager.signoutRedirectCallback();
}
loadSecurityContext() {
this.httpClient.get<AuthContext>(`${Constants.apiRoot}Account/AuthContext`).subscribe(context => {
this.authContext = context;
}, error => console.error(Utils.formatError(error)));
}
}
The login redirect file oidc-login-redirect.html goes into the assets folder of Angular 8 project.
<script src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client/1.4.1/oidc-client.min.js"></script>
<script>
var config = {
userStore: new Oidc.WebStorageStateStore({ store: window.localStorage })
};
var mgr = new Oidc.UserManager(config);
mgr.signinRedirectCallback().then(() => {
window.history.replaceState({},
window.document.title,
window.location.origin);
window.location = "/";
}, error => {
console.error(error);
});
</script>
The silent redirect that automatically renews the token is in the file silent-redirect.html in the assets folder:
<script src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client/1.5.1/oidc-client.min.js"></script>
<script>
var config = {
userStore: new Oidc.WebStorageStateStore({ store: window.localStorage })
};
new Oidc.UserManager(config).signinSilentCallback()
.catch((err) => {
console.log(err);
});
</script>
Then, the config in the ThinkTecture IdentityServer looks somewhat like this:
using System.Collections.Generic;
using IdentityServer4;
using IdentityServer4.Models;
namespace SecuringAngularApps.STS
{
public class Config
{
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("projects-api", "Projects API")
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "spa-client",
ClientName = "Projects SPA",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RequireConsent = false,
RedirectUris = { "http://localhost:4200/assets/oidc-login-redirect.html","http://localhost:4200/assets/silent-redirect.html" },
PostLogoutRedirectUris = { "http://localhost:4200/?postLogout=true" },
AllowedCorsOrigins = { "http://localhost:4200/" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"projects-api"
},
IdentityTokenLifetime=120,
AccessTokenLifetime=120
},
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:4201/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:4201/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
},
AllowOfflineAccess = true
}
};
}
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
}
}
}
And in the API backend we wire up the STS like this in Asp.net Core in the Startup.cs:
using System;
using System.Linq;
using SecuringAngularApps.STS.Data;
using SecuringAngularApps.STS.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.Cookies;
using IdentityServer4.Services;
using SecuringAngularApps.STS.Quickstart.Account;
namespace SecuringAngularApps.STS
{
public class Startup
{
public IConfiguration Configuration { get; }
public IHostingEnvironment Environment { get; }
public Startup(IConfiguration configuration, IHostingEnvironment environment)
{
Configuration = configuration;
Environment = environment;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", corsBuilder =>
{
corsBuilder.AllowAnyHeader()
.AllowAnyMethod()
.AllowAnyOrigin()
.AllowCredentials();
});
});
services.AddTransient<IProfileService, CustomProfileService>();
services.AddMvc();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddAspNetIdentity<ApplicationUser>()
.AddProfileService<CustomProfileService>();
if (Environment.IsDevelopment())
{
builder.AddDeveloperSigningCredential();
}
else
{
throw new Exception("need to configure key material");
}
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseCors("CorsPolicy");
app.UseIdentityServer();
app.UseMvcWithDefaultRoute();
}
}
}