How can I fake keycloack call to use in local development?

Although you explicitly state that you think that mocking is the best option, I suggest to reconsider it in favor of setting up local Keycloak instance using docker. It becomes easy when you provide a realm to bootstrap your environment. I've been using this approach with success for over 2 years of developing applications that work with Keycloak. This approach will let you "substitute calls to your corporate server" hence I post it here.

Assuming that you have docker & docker-compose installed, you'll need:

1. docker-compose.yaml

version: '3.7'

services:
  keycloak:
    image: jboss/keycloak:10.0.1
    environment:
      KEYCLOAK_USER: admin
      KEYCLOAK_PASSWORD: admin
      KEYCLOAK_IMPORT: /tmp/dev-realm.json
    ports:
      - 8080:8080
    volumes:
      - ./dev-realm.json:/tmp/dev-realm.json

2. dev-realm.json (exact content depend on required settings, this is the minimum that you've mentioned in your question)

{
  "id": "dev",
  "realm": "dev",
  "enabled": true,
  "clients": [
    {
      "clientId": "app",
      "enabled": true,
      "redirectUris": [
        "*"
      ],
      "bearerOnly": false,
      "consentRequired": false,
      "standardFlowEnabled": true,
      "implicitFlowEnabled": false,
      "directAccessGrantsEnabled": false,
      "secret": "mysecret",
      "publicClient": false,
      "protocol": "openid-connect",
      "fullScopeAllowed": false,
      "protocolMappers": [
        {
          "name": "department",
          "protocol": "openid-connect",
          "protocolMapper": "oidc-usermodel-attribute-mapper",
          "consentRequired": false,
          "config": {
            "user.attribute": "department",
            "id.token.claim": "true",
            "access.token.claim": "true",
            "claim.name": "department",
            "userinfo.token.claim": "true"
          }
        },
        {
          "name": "employee_number",
          "protocol": "openid-connect",
          "protocolMapper": "oidc-usermodel-attribute-mapper",
          "consentRequired": false,
          "config": {
            "user.attribute": "employee_number",
            "id.token.claim": "true",
            "access.token.claim": "true",
            "claim.name": "employee_number",
            "userinfo.token.claim": "true"
          }
        }
      ]
    }
  ],
  "users": [
    {
      "username": "111111111-11",
      "enabled": true,
      "firstName": "Whatever Something de Paula",
      "email": "[email protected]",
      "credentials": [{
        "type": "password",
        "value": "demo"
      }],
      "attributes": {
        "department": "sales",
        "employee_number": 7777777
      }
    }
  ]
}

3. Create dedicated Angular environment that will use the "http://localhost:8080/auth" and realm "dev" for your local development

The advantages of this approach over mocking:

  • all OIDC and keycloak features are working. I admit that it depends if you need them but you are free to use realm/client roles, groups, 'real' OIDC flow with token refreshal. This gives you guarantee that your local setup will work also with corporate service
  • this setup can be stored in repository (contrary to manual setup of Keycloak server) and used both for working on web applications and backend services

By default, Keycloak uses a H2 in-memory database and needs about 600MB of RAM so I'd argue that it is a relatively low footprint.


Solution

I was able to mock Keycloak service using the method @yurzui suggested. I'll document it here as it may be useful for somebody.

Initially I had posted a solution where I conditionally exported the mock or real classes from the mock module. All worked well on dev mode, but when I tried to build the application for publishing in production server I got an error, so I had to return to the 2 class solution. I explain the problem in details this question .

This is the working code (so far).

Frontend:

With a little help from @kev's answer in this question and @yurzui (again :D) in this one, I created a MockKeycloakService class:

import { Injectable } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { environment } from '../../../environments/environment';

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

    init() {
        console.log('[KEYCLOAK] Mocked Keycloak call');
        return Promise.resolve(true);
    }

    getKeycloakInstance() {
        return {
            loadUserInfo: () => {
                let callback : any;
                Promise.resolve().then(() => {
                    callback({
                        username: '77363698953',
                        NOME: 'Nelson Teixeira',
                        FOTO: 'assets/usuarios/nelson.jpg',
                        LOTACAOCOMPLETA: 'DIOPE/SUPOP/OPSRL/OPSMC (local)',
                    });
                });
                return { success: fn=>callback = fn };
            }
        } as any;
    }

    login() {}  
    logout() {}
} 

const KeycloakServiceImpl =
  environment.production ? KeycloakService : MockKeycloakService

export { KeycloakServiceImpl, KeycloakService, MockKeycloakService }; 

then I substituted it in app.module:

<...>
import { KeycloakAngularModule } from 'keycloak-angular';
import { KeycloakServiceImpl } from 'src/app/shared/services/keycloak-mock.service';
import { initializer } from './app-init';
<...>

    imports: [
        KeycloakAngularModule,
         <...>  
    ],
    providers: [
        <...>,
        {
            provide: APP_INITIALIZER,
            useFactory: initializer,
            multi: true,
            deps: [KeycloakServiceImpl, <...>]
        },
        <...>
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

Then changed the type of keycloak service variable in app-init, that was the only change, but then I could remove KeycloackService import as it's being provided in app.module:

import { KeycloakUser } from './shared/models/keycloakUser';
<...>

export function initializer(
    keycloakService: any,
    <...>
): () => Promise<any> {
    return (): Promise<any> => {
        return new Promise(async (res, rej) => {
            <...>    
            await keycloak.init({
                config: environment.keycloakConfig,
                initOptions: {
                    onLoad: 'login-required',
                    // onLoad: 'check-sso',
                    checkLoginIframe: false
                },
                bearerExcludedUrls: [],
                loadUserProfileAtStartUp: false
            }).then((authenticated: boolean) => {
                if (!authenticated) return;
                keycloak.getKeycloakInstance()
                    .loadUserInfo()
                    .success(async (user: KeycloakUser) => {

                        <...>

                    })    
            }).catch((err: any) => rej(err));
            res();
        });
    };

But in the component I still have to check which environment I'm on and instance the class correctly:

<...>
import { MockKeycloakService } from '../../shared/services/keycloak.mock.service';
import { environment } from '../../../environments/environment';    
<...>

export class MainComponent implements OnInit, OnDestroy {
    <...>
    keycloak: any;

    constructor(
        <...>           
    ) {
        this.keycloak = (environment.production) ? KeycloakServiceImpl : new KeycloakServiceImpl();
  }

    async doLogout() {
        await this.keycloak.logout();
    }

    async doLogin() {
        await this.keycloak.login();
    }
    <...>    
}

Backend:

That was easier, again I created a KeycloakMock class:

import KeyCloack from 'keycloak-connect';

class KeycloakMock {

    constructor(store, config) {
        //ignore them
    }

    middleware() {
        return (req, res, next) =>{ 
        next();
    }}

    protect(req, res, next) {
        return (req, res, next) =>{ 
        next();
    }}
}

const exportKeycloak = 
    (process.env.NODE_ENV == 'local') ? KeycloakMock : KeyCloack;

export default exportKeycloak; 

Then I just substitued 'keycloak-connect' import on app.js by this class, and everythig worked fine. It connects to the real service if I set production = true and it mocks it with production = false.

Very cool solution. If anyone has anything to say on my implementation of @yurzui idea I'll like to hear from you.

Some notes:

  • I still couln't get rid of having to check the environment in the main-component class, as if I do this in the mock class module:

    const KeycloakServiceImpl = 
        environment.production ? KeycloakService : new MockKeycloakService()
    

    app.module doesn't work anymore. and if I do this in main-component:

    constructor(
            <...>
            keycloakService: KeyclockServiceImpl;
        ) {  }
    

    The build fails with a "KeyclockServiceImpl refers to a value but is being used as a type here";

  • I had to export all classes or the build fails

    export { KeycloakServiceImpl, KeycloakService, MockKeycloakService }; 
    

You can leverage Angular environment(or even process.env) variable to switch between real and mock implementations.

Here is a simple example of how to do that:

app-init.ts

...
import { environment } from '../environments/environment';

export function initializer(
  keycloak: KeycloakService
): () => Promise<any> {

  function authenticate() {
    return keycloak
      .init({
        config: {} as any,
        initOptions: {onLoad: 'login-required', checkLoginIframe: false},
        bearerExcludedUrls: [],
        loadUserProfileAtStartUp: false
      })
      .then(authenticated => {
        return authenticated ? keycloak.getKeycloakInstance().loadUserInfo() : Promise.reject();
      });
  }

  // we use 'any' here so you don't have to define keyCloakUser in each environment    
  const { keyCloakUser } = environment as any; 

  return () => {
    return (keyCloakUser ? Promise.resolve(keyCloakUser) : authenticate()).then(user => {
      // ...
      // do whatever you want with user
      // ...
    });
  };
}

environment.ts

export const environment = {
  production: false,
  keyCloakUser: {
    username: '111111111-11',
    name: 'Whatever Something de Paula',
    email: '[email protected]',
  }
};

environment.prod.ts

export const environment = {
  production: true,
};

Update

If you want to mock KeycloakService on client side then you can tell Angular dependency injection to handle that:

app.module.ts

import { environment } from '../environments/environment';
import { KeycloakService, KeycloakAngularModule } from 'keycloak-angular';
import { MockedKeycloakService } from './mocked-keycloak.service';

@NgModule({
  ...
  imports: [
    ...
    KeycloakAngularModule
  ],
  providers: [
    {
      provide: KeycloakService,
      useClass: environment.production ? KeycloakService : MockedKeycloakService
    },
    {
      provide: APP_INITIALIZER,
      useFactory: initializer,
      multi: true,
      deps: [KeycloakService]
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

mocked-keycloak.service.ts

import { Injectable} from '@angular/core';
import { KeycloakService } from 'keycloak-angular';

@Injectable()
class MockedKeycloakService extends KeycloakService {
  init() {
    return Promise.resolve(true);
  }

  getKeycloakInstance() {
    return {
      loadUserInfo: () => {
        let callback;
        Promise.resolve().then(() => {
          callback({
            userName: 'name'
          });
        });
        return {
          success: (fn) => callback = fn
        };
      }
    } as any;
  }
}