Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

My company uses Keycloak for authentication connected with LDAP and returning a user object filled with corporative data. Yet in this period we are all working from home and in my daily work having to authenticate in my corporative server every time I reload the app, has proven to be an expensive overhead. Especially with intermittent internet connections.

How can I fake the Keycloak call and make keycloak.protect() work as it has succeeded?

I can install a Keyclock server in my machine, but I'd rather not do that because it would be another server running in it besides, vagrant VM, Postgres server, be server, and all the other things I leave open. It would be best to make a mock call and return a fixed hard-coded object.

My project's app-init.ts is this:

import { KeycloakService } from 'keycloak-angular';
import { KeycloakUser } from './shared/models/keycloakUser';
<...>

export function initializer(
    keycloak: KeycloakService,
    <...>
): () => 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) => {
                       // ...
                       // load authenticated user data
                       // ...
                    })    
            }).catch((err: any) => rej(err));
            res();
        });
    };

I just need one fixed logged user. But it has to return some fixed customized data with it. Something like this:

{ username: '111111111-11', name: 'Whatever Something de Paula',
  email: '[email protected]', department: 'sales', employee_number: 7777777 }

EDIT

I tried to look at the idea of @BojanKogoj but AFAIU from Angular Interceptor page and other examples and tutorials, it has to be injected in a component. Keycloak initialization is called on app initialization, not in a component. Also Keycloak's return is not the direct return of init() method. It passes through other objects in the .getKeycloakInstance().loadUserInfo().success() sequence. Or maybe it's just me that didn't fully understand it. If anyone can come with an example of an interceptor that can intercept the call and return the correct result, that could be a possibility.

Edit2

Just to complement that what I need is for the whole keycloak's system to work. Please notice that the (user: KeycloakUser) => { function is passed to success method of keycloak's internal system. As I said above, routes have a keycloak.protect() that must work. So it's not just a simple case of returning a promise with a user. The whole .getKeycloakInstance().loadUserInfo().success() chain has to be mocked. Or at least that's how I understand it.

I included an answer with the solution I made based on @yurzui's answer

Will wait a couple of days to award the bounty to see if someone can came up with an even better solution (which I doubt).

like image 594
Nelson Teixeira Avatar asked May 20 '20 16:05

Nelson Teixeira


2 Answers

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.

like image 76
dchrzascik Avatar answered Sep 21 '22 19:09

dchrzascik


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;
  }
}
like image 23
yurzui Avatar answered Sep 21 '22 19:09

yurzui