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