The quest continues, this time to get an Angular client authenticated against our Identity server.
First of all, let's get a basic Angular app going:
$ ng new AngClient --routing -prefix ac --skip-install
$ cd AngClient
$ yarn or npm install
With that, we can now follow another great post by Scott Brady - SPA Authentication using OpenID Connect Angular CLI and oidc client, slightly dated again, however, we can happily follow this guide up untill "Calling a Protected API". I would recommend some changes though to maintain a good code structure, here are my amended instructions.
Protecting a Component & Route Guard
The main problem with the structure that I have here is that everything gets merged together, plus you may have pages that don't need authenticating so let's do the following in the CLI:
$ ng g c components/protects
$ ng g c components/home
Then in the app-routing.module.ts, update the routes array to look like the following:
import { ProtectedComponent } from './components/protected/protected.component';
import { HomeComponent } from './components/home/home.component';
const routes: Routes = [
{
path: '',
component: HomeComponent
},
{
path: 'protected',
component: ProtectedComponent
}
];
Next, let's configure the app.component.html to display the routes through the following no thrills navigation menu:
<h1>
{{title}}
</h1>
<router-outlet></router-outlet>
Route Guard
The route guard is more of a shared component so let's make it so. Firstly let's create a shared module:
$ ng g m shared
Then let's move the common modules from app.module.ts into the shared module, export them and then import the shared module into app.module.
# shared.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BrowserModule } from '@angular/platform-browser';
@NgModule({
imports: [
CommonModule
],
exports : [
CommonModule,
BrowserModule
],
declarations: []
})
export class SharedModule { }
# app.module.ts
...
import { SharedModule } from './shared/shared.module';
@ngModule({
...,
imports: [
AppRoutingModule,
SharedModule
]
}
)
Now let's create our Authguard service and implement it:
$ ng g s shared/services/authguard
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
@Injectable()
export class AuthGuardService implements CanActivate {
canActivate(): boolean {
return false;
}
}
Then declare it as a provider in the shared.module.ts:
providers: [AuthGuardService]
Finally, we want to use this guard within our routes so that any route that requires our authentication, calls the guard's canActivate function prior to any of the routes components lifecycle hooks.
#app-routing.mdoule.ts
{
path: 'protected',
component: ProtectedComponent,
canActivate: [AuthGuardService]
}
If you run the application now, you should no longer be able to access the protected route!
The oidc-client
Now onto the authentication, first of all, create another service called auth and add it into out sharedModule providers.
$ ng g s shared/services/auth
#shared.module.ts
import { AuthService } from './services/auth.service';
...
providers: [AuthGuardService, AuthService]
Next up we need the oidc-client package, through the command line type:
yard add oidc-client
or
npm install oidc-client
We can now use the user manager components from oidc-client in our auth service:
#auth.service.ts
import { UserManager, UserManagerSettings, User } from 'oidc-client';
Next up we need to sort out out client settings to tie in with our identity server. In the auth service add the following code, filling in the authority field with your identity server endpoint address:
#auth.service.ts
@Injectable()
export class AuthService {
}
export function getClientSettings(): UserManagerSettings {
return {
authority: 'https://localhost:443xx/',
client_id: 'ng',
redirect_uri: 'http://localhost:4200/auth-callback',
post_logout_redirect_uri: 'http://localhost:4200/',
response_type: "id_token token",
scope: "openid profile",
filterProtocolClaims: true,
loadUserInfo: true,
automaticSilentRenew: true,
silent_redirect_uri: 'http://localhost:4200/silent-refresh.html'
};
Later down the line you should configure these settings to a config file! Next up, we want to make sure we have a user on the construction of our AuthService with the following:
#auth.service.ts
private manager: UserManager = new UserManager(getClientSettings());
private user: User = null;
constructor() {
this.manager.getUser().then(user => {
this.user = user;
});
}
Then finally for our auth service, we need to add five self-explained functions, if you need more info on these functions have a look at Scott Brady's post which will go into more detail.
#auth.service.ts
isLoggedIn(): boolean {
return this.user != null && !this.user.expired;
}
getClaims(): any {
return this.user.profile;
}
getAuthorizationHeaderValue(): string {
return `${this.user.token_type} ${this.user.access_token}`;
}
startAuthentication(): Promise<void> {
return this.manager.signinRedirect();
}
completeAuthentication(): Promise<void> {
return this.manager.signinRedirectCallback().then(user => {
this.user = user;
});
}
Now we can use the AuthService in our AuthGuardService by implementing the isLoggedIn and startAuthentication function. They are used within the constructor and canActivate function:
#authguard.service.ts
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { AuthService } from './auth.service';
@Injectable()
export class AuthGuardService implements CanActivate {
constructor(private authService: AuthService) { }
canActivate(): boolean {
if(this.authService.isLoggedIn()) {
return true;
}
this.authService.startAuthentication();
return false;
}
}
There is one final task within the angular app, that is to handle the callback endpoint from our identity server and complete the authentication process. We will need a component to do so, so let's create it:
ng g c shared/components/auth-callback -m shared
Then fill it in with the following:
import { Component, OnInit } from '@angular/core';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'app-auth-callback',
templateUrl: './auth-callback.component.html',
styleUrls: ['./auth-callback.component.css']
})
export class AuthCallbackComponent implements OnInit {
constructor(private authService: AuthService) { }
ngOnInit() {
this.authService.completeAuthentication();
}
}
Make sure we're exporting it in our shared.module.ts
#shared.module.ts
import { AuthCallbackComponent } from './components/auth-callback/auth-callback.component';
...
exports : [ ...,
AuthCallbackComponent
]
And finally set up a route so that our Identity Server can reach our Angular app's callback component:
#app-routing.module.ts
const routes: Routes = [
...,
{
path: 'auth-callback',
component: AuthCallbackComponent
}]
And that's it for our Angular app, however, we still need to add it as a client within our Identity Server. Open up our Identity Server project and head to the config.cs file. Then within clients add the following:
#config.cs
new Client
{
ClientId = "ng",
ClientName = "Angular 4 Client",
AllowedGrantTypes = GrantTypes.Implicit,
RedirectUris = new List<string> { "http://localhost:4200/auth-callback" },
PostLogoutRedirectUris = new List<string> { "http://localhost:4200/" },
AllowedCorsOrigins = new List<string> { "http://localhost:4200" },
AllowAccessTokensViaBrowser = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
}
}
Good news we are finally there! Run the Identity Server project and Angular project together and head to your home page, no problem. Now click on the "Protected" link and you should be redirected to the Identity Server login page. Use your credentials and sign in and surprise, you should get redirected to our auth-callback page. Click on the "Protected" link again and you should now see "protected works!". Awesome we can now block pages from unauthorised users!
That's as far as I'm going to go with this post, however, part 3 will be about angular passing its token onto a standalone API. There are also a couple of things you can do at this point to improve your app listed below. Cheers for reading.
The completed code for this post can be found here.
- You probably want to modify the auth-callback page to record what page redirected you to it. That way once the Identity Server is done you can go straight back to the page they originally tried to access.
- Your token will expire eventually and we have no control over that yet. Check it out here.
Comments