DEV Community

Anthony Ikeda
Anthony Ikeda

Posted on • Updated on

Securing Angular and Quarkus with Keycloak Pt 1

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Once Keycloak and PostgreSQL has started you can access the first user interface at http://localhost:8081:
Alt Text

From here we want to access the Administration Console:
Alt Text

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:
Alt Text

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":

Alt Text

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:

Alt Text

Alt Text

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

Alt Text

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.
Enter fullscreen mode Exit fullscreen mode

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)
$
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

app-nav.component.html

<p>Store Details</p>
<ul>
  <li>Inventory</li>
  <li>Sales</li>
  <li>Reporting</li>
</ul>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

If we run our application, we should see both items being displayed:
Alt Text

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:

Alt Text

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:

Alt Text

Next, in the top navigation, click Roles:

Alt Text

Let's create 2 new Roles:

  • customer
  • store-employee

Alt Text

Now, we want to map our roles to the groups we created earlier. Click on Groups in the left navigation:

Alt Text

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.

Alt Text

Do the same for the Store Employees group, but select store-employee as the role to add.

Alt Text

If you click on Members, you should see the users we assigned to the Groups in the beginning:

Alt Text

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.
Enter fullscreen mode Exit fullscreen mode

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'
  }
};
Enter fullscreen mode Exit fullscreen mode

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)
$
Enter fullscreen mode Exit fullscreen mode

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);
      }
    });
  };
}
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode

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:

Alt Text

You can use any of the login credentials you created earlier to then authenticate and be redirected to our app.component.html page:

Alt Text

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();
Enter fullscreen mode Exit fullscreen mode

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}`;
    });
  }

}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

Alt Text

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;
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
alimahdavi profile image
ali mahdavi • Edited

where is other parts?

Collapse
 
anthonyikeda profile image
Anthony Ikeda • Edited