web-dev-qa-db-fra.com

Comment puis-je simuler un appel keycloak à utiliser dans le développement local?

Mon entreprise utilise Keycloak pour l'authentification connectée avec LDAP et renvoyant un objet utilisateur rempli de données corporatives. Pourtant, pendant cette période, nous travaillons tous à domicile et dans mon travail quotidien, devoir m'authentifier sur mon serveur corporatif à chaque fois que je recharge l'application s'est avéré être une surcharge coûteuse. Surtout avec des connexions Internet intermittentes.

Comment puis-je simuler l'appel Keycloak et faire fonctionner keycloak.protect () comme il a réussi?

Je peux installer un serveur Keyclock sur ma machine, mais je préfère ne pas le faire car ce serait un autre serveur qui y fonctionnerait en plus, une VM vagabonde, un serveur Postgres, un serveur et toutes les autres choses que je laisse ouvertes. Il serait préférable de faire un faux appel et de renvoyer un objet fixe codé en dur.

Le fichier app-init.ts de mon projet est le suivant:

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

J'ai juste besoin d'un utilisateur connecté fixe. Mais il doit renvoyer des données personnalisées fixes avec lui. Quelque chose comme ça:

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

MODIFIER

J'ai essayé de regarder l'idée de @BojanKogoj mais AFAIU de Angular Interceptor page et autres exemples et tutoriels, il doit être injecté dans un composant. L'initialisation de Keycloak est appelée lors de l'initialisation de l'application, pas dans un composant. Le retour de Keycloak n'est pas non plus le retour direct de la méthode init (). Il passe par d'autres objets dans la séquence .getKeycloakInstance().loadUserInfo().success(). Ou peut-être que c'est juste moi qui ne l'ai pas bien compris. Si quelqu'un peut venir avec un exemple d'intercepteur capable d'intercepter l'appel et de renvoyer le résultat correct, cela pourrait être une possibilité.

Modifier2

Juste pour compléter cela, ce dont j'ai besoin, c'est que tout le système de Keycloak fonctionne. Veuillez noter que la fonction (user: KeycloakUser) => { Est passée à la méthode success du système interne de keycloak. Comme je l'ai dit ci-dessus, les routes ont un keycloak.protect () qui doit fonctionner. Il ne s'agit donc pas simplement de renvoyer une promesse à un utilisateur. Toute la chaîne .getKeycloakInstance (). LoadUserInfo (). Success () doit être simulée. Ou du moins c'est comme ça que je le comprends.

J'ai inclus une réponse avec la solution que j'ai faite basée sur la réponse de @ yurzui

Attendrons quelques jours pour attribuer la prime pour voir si quelqu'un peut trouver une solution encore meilleure (ce dont je doute).

10
Nelson Teixeira

Bien que vous déclariez explicitement que vous pensez que la moquerie est la meilleure option, je suggère de la reconsidérer en faveur de la configuration d'une instance Keycloak locale à l'aide de docker. Cela devient facile lorsque vous fournissez un royaume à bootstrap votre environnement. J'utilise cette approche avec succès depuis plus de 2 ans de développement d'applications qui fonctionnent avec Keycloak. Cette approche vous permettra de "remplacer appels à votre serveur d'entreprise "donc je le poste ici.

En supposant que docker et docker-compose soient installés, vous aurez besoin de:

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 (le contenu exact dépend des paramètres requis, c'est le minimum que vous avez mentionné dans votre 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. Créez un environnement Angular dédié qui utilisera le " http: // localhost: 8080/auth " et le royaume "dev" pour votre développement local

Les avantages de cette approche par rapport à la moquerie:

  • toutes les fonctionnalités OIDC et keycloak fonctionnent. J'admets que cela dépend si vous en avez besoin, mais vous êtes libre d'utiliser des rôles de domaine/client, des groupes, un flux OIDC `` réel '' avec un rafraîchissement de jeton. Cela vous donne la garantie que votre configuration locale fonctionnera également avec le service d'entreprise
  • cette configuration peut être stockée dans un référentiel (contrairement à la configuration manuelle du serveur Keycloak) et utilisée à la fois pour travailler sur des applications Web et des services backend

Par défaut, Keycloak utilise une base de données en mémoire H2 et a besoin d'environ 600 Mo de RAM donc je dirais qu'il s'agit d'un encombrement relativement faible.

2
dchrzascik

Solution

J'ai pu me moquer du service Keycloak en utilisant la méthode suggérée par @yurzui. Je vais le documenter ici car cela peut être utile pour quelqu'un.

Au départ, j'avais publié une solution dans laquelle j'exportais conditionnellement les classes fictives ou réelles du module fictif. Tout fonctionnait bien en mode développement, mais lorsque j'ai essayé de créer l'application pour la publication sur le serveur de production, j'ai eu une erreur, j'ai donc dû revenir à la solution à 2 classes. J'explique le problème en détails this question.

Ceci est le code de travail (jusqu'à présent).

Frontend:

Avec un peu d'aide de la réponse de @ kev dans this question et @yurzui (encore: D) dans this one, j'ai créé une classe MockKeycloakService:

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

puis je l'ai remplacé dans 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 { }

Ensuite, j'ai changé le type de variable de service keycloak dans app-init, c'était le seul changement, mais j'ai ensuite pu supprimer l'importation KeycloackService telle qu'elle est fournie dans 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();
        });
    };

Mais dans le composant, je dois toujours vérifier dans quel environnement je suis et instancier correctement la classe:

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

C'était plus facile, encore une fois j'ai créé une classe KeycloakMock:

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; 

Ensuite, je viens de remplacer l'importation 'keycloak-connect' sur app.js par cette classe, et tout a bien fonctionné. Il se connecte au service réel si je règle production = true et se moque de lui avec production = false.

Solution très cool. Si quelqu'un a quelque chose à dire sur ma mise en œuvre de l'idée @yurzui, j'aimerais avoir de vos nouvelles.

Quelques notes:

  • Je ne peux toujours pas me débarrasser de la vérification de l'environnement dans la classe du composant principal, comme si je faisais cela dans le module de classe fictive:

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

    app.module ne fonctionne plus. et si je fais cela dans le composant principal:

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

    La construction échoue avec un "KeyclockServiceImpl fait référence à une valeur mais est utilisé comme type ici";

  • J'ai dû exporter toutes les classes ou la construction échoue

    export { KeycloakServiceImpl, KeycloakService, MockKeycloakService }; 
    
1
Nelson Teixeira