Apologies
First I want to apologise to my readers, it seems a whole chunk of my article went missing while trying to post the sample code and escape the curly braces that ended up clashing with the liquid tags.
I've restore the missing content below.
Let's get into the article!
This will be part 1 of a series of posts where we will secure a stack from an Angular 10 UI to a backend resource server with Keycloak.
In this article, we will cover the initial user interface creating a realm, groups, users, permissions and a client for the UI to use as authentication and authorization.
The code for this first article is here: https://github.com/cloudy-engineering/pet-store-ui
First we need to set up our environment which will require the following:
- docker-compose
- Angular 10
- Angular cli
Auth Model
The overall Auth model we will be setting up for this series consists of 2 main components:
- A client for Single sign on for our User Interface
- A client for the Resource Server and mapping it to our single sign on model
Setting up Keycloak
We will be using the dockerized version and we will also ensure we have a persistent state using PostgreSQL as our database.
version: "3.8"
services:
keycloak:
image: jboss/keycloak:latest
environment:
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: superSecret
DB_VENDOR: postgres
DB_ADDR: keycloak-db
DB_DATABASE: keycloak
DB_USER: keycloak
DB_PASSWORD: keycloak
depends_on:
- keycloak-db
ports:
- 8081:8080
keycloak-db:
image: postgres:alpine
environment:
POSTGRES_PASSWORD: keycloak
POSTGRES_USER: keycloak
POSTGRES_DB: keycloak
volumes:
- ./postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 10s
timeout: 5s
retries: 5
To start up Keycloak, navigate to this file and run:
$ docker-compose up
...
INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: Keycloak 10.0.2 (WildFly Core 11.1.1.Final) started in 21588ms - Started 690 of 995 services (708 services are lazy, passive or on-demand)
Once Keycloak and PostgreSQL has started you can access the first user interface at http://localhost:8081:
From here we want to access the Administration Console:
The username and password were defined in the docker-compose.yml file as:
- Username: admin
- Password: superSecret
This will take you to our initial master realm:
Create a new Realm
Since we are starting from scratch with this article, we will create a brand new realm to work with. On the 'Master' drop down select "Add Realm":
We will call this new realm petshop-realm and click Create.
Petshop Realm
The petshop realm will be how we manage our applications and users. Here we will configure:
- Users
- Groups
- Permissions
- Client identifiers
These will connect up our User Interface, backend service and the users we manage in our realm.
First let's create some Users. Select 'Users' under the left navigation Manage section and create a couple of users:
For each user, select Edit → Credentials then add a password (e.g. letmein) and deactivate Temporary Password. Click Set Password.
Next we will create 2 groups:
- customers
- store-employees
Once the groups have been created, let's add some user. For the store-employees group, add Bob Small. To our customers group, let's add Charlene and Mary.
Before we do any more in Keycloak, let's create a quick store app with Angular 10.
Pet Store UI
In this article we will start with the user interface and enable the users we created to login and have the application enable certain functions based on the group the user belongs to.
Let's get our Angular app primed:
$ ng new pet-store --routing --style=css
CREATE pet-store/README.md (1026 bytes)
CREATE pet-store/.editorconfig (274 bytes)
CREATE pet-store/.gitignore (631 bytes)
...
CREATE pet-store/e2e/src/app.po.ts (301 bytes)
✔ Packages installed successfully.
Successfully initialized git.
Next we are going to create the initial home page. The home page will perform the following tasks:
- If the user is a customer, show a left navigation to browse the store
- If the user is a store-employee, the left navigation will show links to inventory
But first, open the project in the IDE of your choice, open the app.component.html page and remove everything. For now we won't be using routing.
Next create 2 components:
$ ng g c store-nav
CREATE src/app/store-nav/store-nav.component.css (0 bytes)
CREATE src/app/store-nav/store-nav.component.html (24 bytes)
CREATE src/app/store-nav/store-nav.component.spec.ts (641 bytes)
CREATE src/app/store-nav/store-nav.component.ts (286 bytes)
UPDATE src/app/app.module.ts (485 bytes)
$ ng g c admin-nav
CREATE src/app/admin-nav/admin-nav.component.css (0 bytes)
CREATE src/app/admin-nav/admin-nav.component.html (24 bytes)
CREATE src/app/admin-nav/admin-nav.component.spec.ts (641 bytes)
CREATE src/app/admin-nav/admin-nav.component.ts (286 bytes)
UPDATE src/app/app.module.ts (577 bytes)
$
We will create a very quick customization of both as below:
store-nav.component.html
<p>Store Navigation</p>
<ul>
<li>Latest Deals</li>
<li>Puppies</li>
<li>Kittens</li>
<li>Exotic pets</li>
</ul>
app-nav.component.html
<p>Store Details</p>
<ul>
<li>Inventory</li>
<li>Sales</li>
<li>Reporting</li>
</ul>
Next, let's add these components to our app.component.html page:
app.component.html
<app-store-nav></app-store-nav>
<app-admin-nav></app-admin-nav>
If we run our application, we should see both items being displayed:
Adding Login Capability
For now that's as far as we can go with the User Interface. We next need to set up Keycloak to enable people to login to the application.
Access the Keycloak admin console and go to clients:
Click on Create
For our new client, we will name it petstore-portal
. For the Root URL we will use http://localhost:4200
for now. Click Save.
Give the new client a name and set the Login Theme to Keycloak:
Next, in the top navigation, click Roles:
Let's create 2 new Roles:
- customer
- store-employee
Now, we want to map our roles to the groups we created earlier. Click on Groups in the left navigation:
And first select the customers group and click Edit.
Click on Role Mappings then in the Client Roles text input, type/select petstore-portal
In the Available Roles click Customer and then click Add Selected.
Do the same for the Store Employees group, but select store-employee as the role to add.
If you click on Members, you should see the users we assigned to the Groups in the beginning:
Great! Now it's time to wire up our user interface to let the people in!
Enabling Single Sign On
For connectivity to Keycloak, we are going to use the keycloak-angular and keycloak-js libraries:
$ yarn add keycloak-angular keycloak-js@10.0.2
yarn add v1.22.5
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 3 new dependencies.
info Direct dependencies
├─ keycloak-angular@8.0.1
└─ keycloak-js@10.0.2
info All dependencies
├─ js-sha256@0.9.0
├─ keycloak-angular@8.0.1
└─ keycloak-js@11.0.2
✨ Done in 4.99s.
NOTE
When adding keycloak-js
make sure you match up the correct version of the library with the version of Keycloak you are running.
If you find 404's popping up when trying to login in to your Angular application then this is probably the reason.
You can check the version of your Keycloak server by accessing: Admin → Server Info
It will be the first entry as Server Version.
Next let's set up some configuration parameters. Open the src/environment/environment.ts
file and add the following configuration:
export const environment = {
production: false,
keycloak: {
issuer: 'http://localhost:8081/auth/',
realm: 'petshop-realm',
clientId: 'petstore-portal'
}
};
Next we want to load this configuration so we create an initializer:
$ ng g s initializer
CREATE src/app/initializer.service.spec.ts (382 bytes)
CREATE src/app/initializer.service.ts (140 bytes)
$
Now let's implement the initializer:
initializer.service.ts
import { KeycloakService } from 'keycloak-angular';
import { environment as env} from '../environments/environment';
export function initializer(keycloak: KeycloakService): () => Promise<any> {
return (): Promise<any> => {
return new Promise(async (resolve, reject) => {
try {
await keycloak.init({
config: {
url: env.keycloak.issuer,
realm: env.keycloak.realm,
clientId: env.keycloak.clientId,
},
loadUserProfileAtStartUp: true,
initOptions: {
onLoad: 'login-required'
},
bearerExcludedUrls: []
});
resolve();
} catch(error) {
reject(error);
}
});
};
}
So what are we doing here? On Line 8, we are initializing the KeyCloak service with the settings we placed in our environment.ts
file. This sets the auth server we plan to use (localhost:8081
), the realm (petshop-realm
) and the client (petstore-portal
). We are also instructing Keycloak to load the User Profile at startup and ensure the user logs in initially.
One last thing we need to do is bootstrap our initializer in the app.module.ts
file:
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { StoreNavComponent } from './store-nav/store-nav.component';
import { AdminNavComponent } from './admin-nav/admin-nav.component';
import { initializer } from './initializer.service';
import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular';
@NgModule({
declarations: [
AppComponent,
StoreNavComponent,
AdminNavComponent
],
imports: [
BrowserModule,
AppRoutingModule,
KeycloakAngularModule,
],
providers: [
{
provide: APP_INITIALIZER,
useFactory: initializer,
deps: [KeycloakService],
multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
Here we are importing the KeycloakAngularModule and KeycloakService and making sure we use both.
Now if we build and run our Angular application and access it at http://localhost:4200/
you should find yourself redirected to the Keycloak login page:
You can use any of the login credentials you created earlier to then authenticate and be redirected to our app.component.html
page:
Congrats, you are now securely logged in to your application!
Let's see if we can limit the view to the specified role.
Role Based Views
When we set up Bob earlier, we added him to our store-employees
group. In our Keycloak initializer we indicated we want to load the User Profile when they logged in. Using the Keycloak Service, we can get the roles the user currently belongs to a limit what they can access:
var roles: string[] = this.keycloakService.getUserRoles();
Let's update the app.component.ts
to retrieve the roles and make them accessible to our page:
app.component.ts
import { Component, OnInit } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'pet-store';
roles: string[];
username: string;
constructor(private keycloak: KeycloakService) {}
ngOnInit() {
this.roles = this.keycloak.getUserRoles();
this.keycloak.loadUserProfile().then(profile => {
this.username = `${profile.firstName} ${profile.lastName}`;
});
}
}
Now, let's put some conditions in the UI to only allow the respective roles to access the different lists:
app.component.html
<div *ngIf="username">Welcome username </div>
<div *ngIf="roles.includes('customer')">
<app-store-nav></app-store-nav>
</div>
<div *ngIf="roles.includes('store-employee')">
<app-admin-nav></app-admin-nav>
</div>
As you can see in the app.component.ts
, we've injected the KeycloakService
and use it to get the list of roles the user has (you may see more than the one we allocated). In our user interface we wrap our customer navigation components with a div tag and put conditions in place to limit the visibility to the specified roles. If you access the application in a web browser now you should see only what the user is authorized to access:
TIP
If you are seeing errors in the JavaScript console about Refused to frame 'http://localhost:8081/` then you can correct this by updating the Content-Security-Policy in Keycloak.
Navigate to Realm Settings → Security Settings and update the Content-Security-Policy with:
frame-src 'self' http://localhost:4200; frame-ancestors 'self' http://localhost:4200; object-src none;
This will ensure that, much like CORS, that localhost:4200 can be identified as being able to load content from the Keycloak Server.
That's it for now, in the next article we will wire up the Quarkus Microservice and communicate with it securely using OIDC.
Top comments (2)
where is other parts?
I just published it!
dev.to/anthonyikeda/securing-angul...