DEV Community

Anthony Ikeda
Anthony Ikeda

Posted on

Securing Angular and Quarkus with Keycloak Pt 2

In the previous article we created an initial user interface that enables Keycloak login and role recognition. The next step is to wire this up to a back end service to serve secure, role-based services.


Code for this article is located in Github here:


In a typical scenario I would implement 2 different types services:

  • Services for store employees to use serving up employee specific functions
  • Services for customer specific functions

I'd also implement multiple levels of integration. For now we will start with something simple, create a single Quarkus microservice and manage access through the roles we created.

So, let's kick it off!

Create the API Client

We are going back to the Keycloak admin UI to create a new client for the API to use:

Navigate to Clients, then click Create:

api client

Once the client is created, navigate to the Roles tab to create the roles that we want the client to assume:

api roles

And we want to map these back to our users as in the previous article.

Navigate to Groups -> store-employees -> Role Mappings and add in the api-employee role:

api role mappings employee

Do the same for the api-customer role:

api role mappings customer

Next we want to start work on our service api.

Creating our Service

We will start from scratch and create a standard Quarkus service:

$ mvn io.quarkus:quarkus-maven-plugin:1.7.2.Final:create \
    -DprojectGroupId=com.brightfield.streams \
    -DprojectArtifactId=petstore-api
Enter fullscreen mode Exit fullscreen mode

Some added dependencies that will make life easier include:

    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-resteasy-jackson</artifactId>
    </dependency>
Enter fullscreen mode Exit fullscreen mode

Before we secure our application, let's get something running.

Implementing the Endpoints

There are 3 endpoints we will implement:

  • GET /pets
    • accessible by both groups (store-employees and customers)
  • GET /sales
    • accessible only by the store-employees
  • GET /rewards
    • accessible by the logged in customer

In the project, let's create a couple of resources and representations:

PetResource.java

package com.cloudyengineering.pets;

import java.util.ArrayList;
import java.util.List;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;


@Path("/v1/pets")
@Produces("application/json")
public class PetResource {

    @GET
    public Response getPets() {
        List<Pet> pets = new ArrayList<>();

        return Response.ok(pets).build();
    }
}
Enter fullscreen mode Exit fullscreen mode

SalesResource.java

package com.cloudyengineering.pets;

import java.util.ArrayList;
import java.util.List;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;

@Path("/v1/admin")
@Produces("application/json")
public class SalesResource {

    @GET
    public Response getSales() {
        List<Transaction> transactions = new ArrayList<>();

        return Response.ok(transactions).build();
    }
}
Enter fullscreen mode Exit fullscreen mode

Pet.java

package com.cloudyengineering.pets;

import com.fasterxml.jackson.annotation.JsonProperty;

public class Pet {

    @JsonProperty("pet_id")
    private Integer petId;

    @JsonProperty("pet_type")
    private String petType;

    @JsonProperty("pet_name")
    private String petName;

    @JsonProperty("pet_age")
    private Integer petAge;

    public Integer getPetId() {
        return petId;
    }

    public void setPetId(Integer petId) {
        this.petId = petId;
    }

    public String getPetType() {
        return petType;
    }

    public void setPetType(String petType) {
        this.petType = petType;
    }

    public String getPetName() {
        return petName;
    }

    public void setPetName(String petName) {
        this.petName = petName;
    }

    public Integer getPetAge() {
        return petAge;
    }

    public void setPetAge(Integer petAge) {
        this.petAge = petAge;
    }

}
Enter fullscreen mode Exit fullscreen mode

Transaction.java

package com.cloudyengineering.pets;

import java.util.Date;

import com.fasterxml.jackson.annotation.JsonProperty;

public class Transaction {

    @JsonProperty("txn_id")
    private String transactionId;

    @JsonProperty("txn_amount")
    private Double transactionAmount;

    @JsonProperty("txn_date")
    private Date transactionDate;

    @JsonProperty("txn_method")
    private String transactionMethod;

    public String getTransactionId() {
        return transactionId;
    }

    public void setTransactionId(String transactionId) {
        this.transactionId = transactionId;
    }

    public Double getTransactionAmount() {
        return transactionAmount;
    }

    public void setTransactionAmount(Double transactionAmount) {
        this.transactionAmount = transactionAmount;
    }

    public Date getTransactionDate() {
        return transactionDate;
    }

    public void setTransactionDate(Date transactionDate) {
        this.transactionDate = transactionDate;
    }

    public String getTransactionMethod() {
        return transactionMethod;
    }

    public void setTransactionMethod(String transactionMethod) {
        this.transactionMethod = transactionMethod;
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, these are just placeholders so let's fill in some logic:

    @GET
    public Response getPets() {
        List<Pet> pets = new ArrayList<>();
        Pet pet1 = new Pet();
        pet1.setPetId(1);
        pet1.setPetAge(6);
        pet1.setPetName("Oliver");
        pet1.setPetType("Dog");

        Pet pet2 = new Pet();
        pet2.setPetId(2);
        pet2.setPetAge(1);
        pet2.setPetName("Buster");
        pet2.setPetType("Cat");

        Pet pet3 = new Pet();
        pet3.setPetId(3);
        pet3.setPetAge(2);
        pet3.setPetName("Violet");
        pet3.setPetType("Bird");

        pets = Lists.asList(pet1, new Pet[]{pet2, pet3});

        return Response.ok(pets).build();
    }
Enter fullscreen mode Exit fullscreen mode

TransactionResource.java

    @GET
    public Response getSales() {
        List<Transaction> transactions = new ArrayList<>();

        Transaction txn1 = new Transaction();
        txn1.setTransactionId(UUID.randomUUID().toString());
        txn1.setTransactionAmount(12.56);
        txn1.setTransactionDate(Date.from(Instant.now()));
        txn1.setTransactionMethod("Cash");

        Transaction txn2 = new Transaction();
        txn2.setTransactionId(UUID.randomUUID().toString());
        txn2.setTransactionAmount(56.16);
        txn2.setTransactionDate(Date.from(Instant.now()));
        txn2.setTransactionMethod("Credit Card");

        Transaction txn3 = new Transaction();
        txn3.setTransactionId(UUID.randomUUID().toString());
        txn3.setTransactionAmount(88.99);
        txn3.setTransactionDate(Date.from(Instant.now()));
        txn3.setTransactionMethod("Credit Card");

        transactions = Lists.asList(txn1, new Transaction[]{txn2, txn3});

        return Response.ok(transactions).build();
    }
Enter fullscreen mode Exit fullscreen mode

Let's quickly give it a test!

$ ./mvnw quarkus:dev
Listening for transport dt_socket at address: 5005
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2020-10-04 18:30:09,620 INFO  [io.quarkus] (Quarkus Main Thread) pet-store-api 1.0-SNAPSHOT on JVM (powered by Quarkus 1.8.1.Final) started in 3.296s. Listening on: http://0.0.0.0:
8080
2020-10-04 18:30:09,622 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2020-10-04 18:30:09,622 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy, resteasy-jackson]
Enter fullscreen mode Exit fullscreen mode
$ http :8080/v1/pets
HTTP/1.1 200 OK
Content-Length: 188
Content-Type: application/json

[
    {
        "pet_age": 6,
        "pet_id": 1,
        "pet_name": "Oliver",
        "pet_type": "Dog"
    },
    {
        "pet_age": 1,
        "pet_id": 2,
        "pet_name": "Buster",
        "pet_type": "Cat"
    },
    {
        "pet_age": 2,
        "pet_id": 3,
        "pet_name": "Violet",
        "pet_type": "Bird"
    }
]
Enter fullscreen mode Exit fullscreen mode
$ http :8080/v1/admin
HTTP/1.1 200 OK
Content-Length: 357
Content-Type: application/json

[
    {
        "txn_amount": 12.56,
        "txn_date": 1601862235453,
        "txn_id": "cb09b51d-541a-45b5-9c27-13a09b480dfd",
        "txn_method": "Cash"
    },
    {
        "txn_amount": 56.16,
        "txn_date": 1601862235454,
        "txn_id": "2895fe10-31ae-417a-8d7d-28ccdf0fa08b",
        "txn_method": "Credit Card"
    },
    {
        "txn_amount": 88.99,
        "txn_date": 1601862235454,
        "txn_id": "afbec85c-b919-40e4-9b02-9e5779534b0b",
        "txn_method": "Credit Card"
    }
]
Enter fullscreen mode Exit fullscreen mode

Hmmm... those dates don't look too friendly, let's change them:

Transaction.java

    @JsonProperty("txn_date")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
    private Date transactionDate;
Enter fullscreen mode Exit fullscreen mode

And again...

$ http :8080/v1/admin
HTTP/1.1 200 OK
Content-Length: 381
Content-Type: application/json

[
    {
        "txn_amount": 12.56,
        "txn_date": "05-10-2020 01:46:17",
        "txn_id": "8d5e875b-06ec-4cf6-b357-4e14525b831c",
        "txn_method": "Cash"
    },
    {
        "txn_amount": 56.16,
        "txn_date": "05-10-2020 01:46:17",
        "txn_id": "5764bef6-1c78-4efd-8016-119759b263c5",
        "txn_method": "Credit Card"
    },
    {
        "txn_amount": 88.99,
        "txn_date": "05-10-2020 01:46:17",
        "txn_id": "67a5f766-4a9f-4fa4-8e22-59ecf9171e55",
        "txn_method": "Credit Card"
    }
]
Enter fullscreen mode Exit fullscreen mode

Nice! It works but is isn't quite secure yet.

Securing the API

There will be 2 dependencies we will be adding to secure our service:

pom.xml

  <dependencies>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-oidc</artifactId>
    </dependency>

    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-keycloak-authorization</artifactId>
    </dependency>
  </dependencies>
Enter fullscreen mode Exit fullscreen mode

application.properties

quarkus.oidc.auth-server-url=http://localhost:8081/auth/realms/petshop-realm
quarkus.oidc.client-id=pet-store-api
quarkus.oidc.credentials.secret=itsasecret
quarkus.oidc.authentication.scopes=profile

quarkus.http.cors.origins=http://localhost:4200
quarkus.http.cors.methods=GET,OPTIONS
quarkus.http.cors=true
Enter fullscreen mode Exit fullscreen mode

As you can see we have declared the client id petshop-api but we've also supplied the secret itsasecret, let's make sure that is configured!

Navigate to: Clients -> pet-store-api

Change Access Type to bearer only and hit save. You should see the tabs above change!

bearer token

Next navigate to Credentials in the tabs above and you should see your client secret:

api credentials

Let's copy this generated secret to our Pet Store API:

quarkus.oidc.auth-server-url=http://localhost:8081/auth/realms/petshop-realm
quarkus.oidc.client-id=pet-store-api
quarkus.oidc.credentials.secret=05da844f-c975-4571-8767-cbc8078e7b64
quarkus.oidc.authentication.scopes=profile
Enter fullscreen mode Exit fullscreen mode

If we run our API now and try and access it we should get a 401 Unauthorized:

$ http :8080/v1/admin
HTTP/1.1 200 OK
Content-Length: 381
Content-Type: application/json

[
    {
        "txn_amount": 12.56,
        "txn_date": "06-10-2020 09:34:58",
        "txn_id": "a19f4089-5f20-4355-ab4a-a18367347d6d",
        "txn_method": "Cash"
    },
    {
        "txn_amount": 56.16,
        "txn_date": "06-10-2020 09:34:58",
        "txn_id": "6457746c-9c07-4cea-b8c5-fb30dc4e2f3d",
        "txn_method": "Credit Card"
    },
    {
        "txn_amount": 88.99,
        "txn_date": "06-10-2020 09:34:58",
        "txn_id": "09125408-0787-41d0-9976-8e56c5937bb3",
        "txn_method": "Credit Card"
    }
]
Enter fullscreen mode Exit fullscreen mode

Wait a minute! That's not right! Oh I remember, we haven't specified security around our endpoints! Let's update the code:

SalesResource.java

@Path("/v1/admin")
@Produces("application/json")
public class SalesResource {

    @GET
    @RolesAllowed({"api-employee"})
    public Response getSales() {
        List<Transaction> transactions;

        Transaction txn1 = new Transaction();
        txn1.setTransactionId(UUID.randomUUID().toString());
        txn1.setTransactionAmount(12.56);
        txn1.setTransactionDate(Date.from(Instant.now()));
        txn1.setTransactionMethod("Cash");

        Transaction txn2 = new Transaction();
        txn2.setTransactionId(UUID.randomUUID().toString());
        txn2.setTransactionAmount(56.16);
        txn2.setTransactionDate(Date.from(Instant.now()));
        txn2.setTransactionMethod("Credit Card");

        Transaction txn3 = new Transaction();
        txn3.setTransactionId(UUID.randomUUID().toString());
        txn3.setTransactionAmount(88.99);
        txn3.setTransactionDate(Date.from(Instant.now()));
        txn3.setTransactionMethod("Credit Card");

        transactions = Lists.asList(txn1, new Transaction[]{txn2, txn3});

        return Response.ok(transactions).build();
    }
}
Enter fullscreen mode Exit fullscreen mode

PetResource.java

@Path("/v1/pets")
@Produces("application/json")
public class PetResource {

    @GET
    @RolesAllowed({"api-customer"})
    public Response getPets() {
        List<Pet> pets;
        Pet pet1 = new Pet();
        pet1.setPetId(1);
        pet1.setPetAge(6);
        pet1.setPetName("Oliver");
        pet1.setPetType("Dog");

        Pet pet2 = new Pet();
        pet2.setPetId(2);
        pet2.setPetAge(1);
        pet2.setPetName("Buster");
        pet2.setPetType("Cat");

        Pet pet3 = new Pet();
        pet3.setPetId(3);
        pet3.setPetAge(2);
        pet3.setPetName("Violet");
        pet3.setPetType("Bird");

        pets = Lists.asList(pet1, new Pet[]{pet2, pet3});

        return Response.ok(pets).build();
    }
}
Enter fullscreen mode Exit fullscreen mode

And try again...

$ http :8080/v1/admin
HTTP/1.1 401 Unauthorized
Content-Length: 0
$ http :8080/v1/pets
HTTP/1.1 401 Unauthorized
Content-Length: 0
Enter fullscreen mode Exit fullscreen mode

Great! It's secure!

Ready to integrate the User Interface?

Connecting our UI with the secure services

When we last visited the User Interface, we were able to get user login working as well as ensure that different roles had different views. The changes we are going to make to the UI include:

  • Add a new view to list Pets for customers and employees
  • Add a new view to list Sales for employees
  • Add a new view to list Rewards for customers
  • Add in an AuthGuard to ensure different roles don't try and cheat and bypass the URIs

Creating the Views

Let's start creating new views!

Start by creating the new PetsComponent, SalesComoponent and RewardsComoponent:

$ ng g c pet
CREATE src/app/pet/pet.component.css (0 bytes)
CREATE src/app/pet/pet.component.html (18 bytes)
CREATE src/app/pet/pet.component.spec.ts (605 bytes)
CREATE src/app/pet/pet.component.ts (263 bytes)
UPDATE src/app/app.module.ts (947 bytes)
$ ng g c sales
CREATE src/app/sales/sales.component.css (0 bytes)
CREATE src/app/sales/sales.component.html (20 bytes)
CREATE src/app/sales/sales.component.spec.ts (619 bytes)
CREATE src/app/sales/sales.component.ts (271 bytes)
UPDATE src/app/app.module.ts (1025 bytes)
$ ng g c rewards
CREATE src/app/rewards/rewards.component.css (0 bytes)
CREATE src/app/rewards/rewards.component.html (22 bytes)
CREATE src/app/rewards/rewards.component.spec.ts (633 bytes)
CREATE src/app/rewards/rewards.component.ts (279 bytes)
UPDATE src/app/app.module.ts (1111 bytes)
$ ng g guard auth
? Which interfaces would you like to implement? CanActivate
CREATE src/app/auth.guard.spec.ts (331 bytes)
CREATE src/app/auth.guard.ts (457 bytes)
Enter fullscreen mode Exit fullscreen mode

I'm not going to go full on MVC approach here so we will create 1 service to call the different APIs:

$ ng g s store
CREATE src/app/store.service.spec.ts (352 bytes)
CREATE src/app/store.service.ts (134 bytes)
Enter fullscreen mode Exit fullscreen mode

Let's focus on the 2 main areas:

  • StoreService
  • AuthGuard

StoreService

Based on our original endpoints, lets create 3 functions:

  • getPets(): Observable<Pet[]>
  • getSales(): Observable<Transaction[]>
  • getRewards(): Observable<Reward[]>

store.service.ts

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Pet, Reward, Transaction } from './_model';
import { environment as env } from '../environments/environment';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class StoreService {

  constructor(private http : HttpClient) { }

  getPets(): Observable<Pet[]> {
    const uri = `${env.api_host}/v1/pet`;
    return this.http.get<Pet[]>(uri);
  }

  getSales(): Observable<Transaction[]> {
    const uri = `${env.api_host}/v1/sales`;
    return this.http.get<Transaction[]>(uri);
  }

  getRewards(): Observable<Reward[]> {
    const uri = `${env.api_host}/v1/rewards`;
    return this.http.get<Reward[]>(uri);
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's wire in the service to our components and render the results:

sales.component.ts

import { Component, OnInit } from '@angular/core';
import { StoreService } from '../store.service';
import { map, catchError } from 'rxjs/operators';
import { Transaction } from '../_model';
import { of } from 'rxjs';

@Component({
  selector: 'app-sales',
  templateUrl: './sales.component.html',
  styleUrls: ['./sales.component.css']
})
export class SalesComponent implements OnInit {

  sales: Transaction[];

  constructor(private store: StoreService) { }

  ngOnInit(): void {
    const sale$ = this.store.getSales().pipe(
      map(results => {
        this.sales = results;
      }),
      catchError(error => {
        console.log(error);
        return of([]);
      })
    );

    sale$.subscribe(data => data);
  }

}
Enter fullscreen mode Exit fullscreen mode

sale.component.html

<p>sales works!</p>
<table>
    <tr>
        <th>Transaction ID</th>
        <th>Transaction Date</th>
        <th>Transaction Amount</th>
        <th>Payment Method</th>
    </tr>
    <tr *ngFor="let sale of sales">
        <td>{{sale.txn_id}}</td>
        <td>{{sale.txn_date}}</td>
        <td>{{sale.txn_amount}}</td>
        <td>{{sale.txn_method}}</td>
    </tr>
    <tr *ngIf="sales === undefined">
        <td colspan="4">No data</td>
    </tr>
</table>
Enter fullscreen mode Exit fullscreen mode

pet.component.ts

import { Component, OnInit } from '@angular/core';
import { StoreService } from '../store.service';
import { map } from 'rxjs/operators';
import { Reward } from '../_model';

@Component({
  selector: 'app-rewards',
  templateUrl: './rewards.component.html',
  styleUrls: ['./rewards.component.css']
})
export class RewardsComponent implements OnInit {

  rewards: Reward[];

  constructor(private store: StoreService) { }

  ngOnInit(): void {
    this.store.getRewards().pipe( 
      map(results => {
        this.rewards = results;
      })
    );
  }

}
Enter fullscreen mode Exit fullscreen mode

pet.component.html

<p>pet works!</p>
<table>
    <tr>
        <th>Pet ID</th>
        <th>Pet Type</th>
        <th>Pet Age</th>
        <th>Pet Name</th>
    </tr>
    <tr *ngFor="let pet of pets">
        <td>{{pet.pet_id}}</td>
        <td>{{pet.pet_type}}</td>
        <td>{{pet.pet_age}}</td>
        <td>{{pet.pet_name}}</td>
    </tr>
    <tr *ngIf="pets === undefined">
        <td colspan="4">No data</td>
    </tr>
</table>
Enter fullscreen mode Exit fullscreen mode

As you can see we are storing the hostname for the api in the environment object and constructing our URIs from it.

You can see the source code for the structure of the model classes.

Let's log in as Charlene Masters and see what resolves:

cust pets

cust sales

As you can see we get a result based upon the allocated roles. But how dos the API know what roles the user has? If you open the developer console and select the Network tab, take a look at the XHR Request to the server:

bearer token header

If you take a look, you can see the bearer token being passed as part of the API request!

Because we've mapped the pet-store-api client roles to the Group, these roles are passed across as part of the bearer token to the API, honoring the access the user has.

Setting up our Guard

So in any standard application, Charlene should not be able to access the Sales view at all. In the next section we are going to setup the AuthGuard to check her roles and ensure we decline access to the SalesComponent.

First in order to use Keycloak to guard our route, we need to make some changes to our AuthGuard:

auth.guard.ts

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard extends KeycloakAuthGuard {

  constructor(protected router: Router, protected service: KeycloakService) {
    super(router, service)
  }

  isAccessAllowed(route: ActivatedRouteSnapshot,state: RouterStateSnapshot) : Promise<boolean> {
    return new Promise((resolve, reject) => {
        resolve(true)
    });
  }  
}
Enter fullscreen mode Exit fullscreen mode

Here we are using the Keycloak wrapper to ensure the correct authenticated calls back to the Keycloak server are made.

What's important in this class is the isAccessAllowed() method. This is the entrypoint that will dictate if the user can activate the component.

Let's do a check on the path and see if they should access the sales data:

auth.guard.ts

  isAccessAllowed(route: ActivatedRouteSnapshot,state: RouterStateSnapshot) : Promise<boolean> {
    return new Promise((resolve, reject) => {
      const userRoles: string[] = this.service.getUserRoles();
      console.log(`Roles: ${userRoles}`);

      if (state.url === '/sales' && userRoles.indexOf('employee') >= 0) {
        console.log('Permission allowed');
        resolve(true)
      } else {
        console.log('Permission not allowed');
        resolve(false);
      }

    });
Enter fullscreen mode Exit fullscreen mode

Setting up this guard in our app-routing.module.ts is as simple as:

const routes: Routes = [
  { path: 'pets', component: PetComponent },
  { path: 'rewards', component: RewardsComponent },
  { path: 'sales', component: SalesComponent, canActivate: [AuthGuard] }
];
Enter fullscreen mode Exit fullscreen mode

Now if you access the sales.component as Charlene you should be able to see a message Permission not allowed in the console and the component is never activated.


NOTE

You can see we are printing out the roles the user has and if you take a look you'll notice that
Charlene has the following roles: api-customer,customer,manage-account,manage-account-links,view-profile,offline_access,uma_authorization


So we've now been able to secure our User Interface and our backend services. There is, of course, more you can do with this but it should give you a head start on secure applcations.

In Part 3, we will wire up the RewardsComponent and load user specific information!

Top comments (0)