Open nikosvr88 opened 5 years ago
What about doing the keycloak.init() before bootstraping Angular, as shown here: https://symbiotics.co.za/integrating-keycloak-with-an-angular-4-web-application-part-5/ ? You could add the realm in an environment variable. Doing that way would also let you bootstrap angular in a "offline mode" if you want to skip keycloak authentication during the development phase.
I found an other approach to support multi-tenant. I'm adding the tenant in the url www.my-application.com/tenant-name Then, extract it using a regex. The regex is defined in my environment variable, therefor I can change it more easily. I also added an "offline" mode, sometime usefull in development. Note that I use the term realm
which is corresponding to tenant in Keycloak glossary.
my environment.ts:
export const environment = {
production: false,
offline: false,
defaultRealm: 'my-default-realm',
multiTenant: true,
realmRegExp: '^https?:\\/\\/[^\\/]+\\/([-a-z0-9]+)',
baseUrl: '/',
};
app-init.ts:
import { KeycloakService } from 'keycloak-angular';
import { environment } from '../../environments/environment';
export function initializer(keycloak: KeycloakService): () => Promise<any> {
// Check offline mode
if (environment.offline) {
return (): Promise<any> => Promise.resolve();
}
// Set default realm
let realm = environment.defaultRealm;
// Check multi-tenant
if (environment.multiTenant) {
const matches: RegExpExecArray = new RegExp(environment.realmRegExp).exec(window.location.href);
if (matches) {
realm = matches[1];
console.log('Realm found', realm);
// Update the <base> href attribute
document.getElementsByTagName('base').item(0).attributes.getNamedItem('href').value = environment.baseUrl + realm + '/';
} else {
// Here you can redirect user to an error page.
console.error('Realm not found');
return;
}
}
return (): Promise<any> => keycloak.init({
config: {
url: 'http://localhost:8080/auth',
realm: realm,
clientId: '<keycloak-client-id>'
},
initOptions: {
onLoad: 'login-required',
checkLoginIframe: false
}
});
}
@nikosvr88, I didn't try to use keycloak-angular for this scenario, however I do think it is possible using the angular modular approach. I would not initialise the KeycloakService instance at the AppModule. I would instead create a new module for each tenant, so your application would look like this.
For this approach you should not use the APP_INITIALIZER. The reason for using it is to ensure that at the beginning of your app initialisation you would have the flow redirected to login, if desired, and also the user details and roles. But as I mentioned, it all depends on your workflow and web app needs.
I will try to create an example for this, okay?
Thanks
@mauriciovigolo any update on this please, probably a sample which I could use?
FYI, we've been using for over a year a similar approach to what @dsnoeck suggested in his last comments. The main difference in our app is that the keycloak realm configs are fetched from our back-end.
Everything works nicely except if you pass a bad realm configuration (with for instance a bad realm name).
I know this is an edge case, because there is no reason for the configuration to be wrong ; but still, I'd like this to be handled more properly. Currently, providing a wrong config to the init() function of keycloak-angular's library will result in a 404 while trying to get the login-status-iframe.html ; and no proper way to catch the error :-(
I had talked with @mauriciovigolo on the slack linked to the lib but we did not find a solution. Any inputs are welcomed.
@IAmEilminx thanks for your comment. In the meantime, I have extracted my code it into an Angular Library to be reused in several webapps. Unfortunately, doing that I was not able to keep the the offline feature. Do you have an offline mode working ? If you are interested about my approach, here is the code. I'm new to Angular, so I'm not sure this approach is the best.
In the library I have my initializer function in a service:
export class AaaService {
constructor(
private keycloak: KeycloakService,
@Inject('KEYCLOAK_REALM') private keycloakRealm: string,
@Inject('ENV') private env: Env,
) {}
public initializer(): Promise<boolean> {
// URL to fetch the keycloak config
const keycloakConfig = `${this.env.ApiUrl}/${this.env.apiPathPrefix}/realms/${this.keycloakRealm}/client/config`;
return this.keycloak.init({
config: keycloakConfig,
initOptions: { onLoad: 'login-required' },
enableBearerInterceptor: true
});
}
export function getRealm(): string {
// Get the realm either using env variable or window.location.href
}
@NgModule({ ... providers: [ KeycloakService, { provide: 'KEYCLOAK_REALM', useValue: getRealm(), }, { provide: APP_BASE_HREF, // required as I retrieve the realm from the URL useValue: '/' + getRealm() } ], ... })
3. and finally in my webapp app.module.ts:
@NgModule({
...
providers: [
{provide: 'ENV', useValue: merge(env, environment)},
{
provide: APP_INITIALIZER,
useFactory(aaaService: AaaService) {
return (): Promise
@dsnoeck Sadly no, we did not implement an offline mode as our Backend service require the AuthToken to function.
Is your approach working if you need to connect with another realm within a same session ?
In our app, we have a public page set to the root with an input that lets the user connect to his specific Realm. Also, if this user was to navigate directly from a URL (for instance dashboard.com/userRealm) our AuthGuards would trigger the initialization of our service & keycloak with the correct configuration.
Our flow goes something like: -> Load dashboard.com/realmName -> AuthGuard triggers -> Match realmName from url -> Retrieve list of realms and their configs from our Backend -> Match the correct config with the target realm -> Initialize Keycloak -> Initialize rest of the app (Setting Themes, logos, etc)
@dsnoeck thanks for the configurations, I have managed to handle multi-tenancy by checking for the realm in the start of sub domain in the url. For example, org1.sub.domain, therefore, the org1 with be the realm in this case.
Also I have created a re-usable angular library for the Keycloak initialization and for the auth guard implementation.
However, what I am concerned is that I would like to redirect a user on login for another app, which depends on the logging-in user's default application which is set in keycloak as custom attribute.
The problem is that let's say I'm logging-into app1, on login success I have to be re-directed to my default application which is not app1, assume its app2, which as I mentioned above depends on the default application value set for each user.
Any ideas on how I can move forward in this case?
Thanks in advance.
@kasibkismath I see one option: The options to be passed to the login method of keycloak-angular service (see: https://github.com/mauriciovigolo/keycloak-angular/blob/master/projects/keycloak-angular/src/lib/core/services/keycloak.service.ts#L256) contains a redirectUri. You could use that to define a URL where you would check user's attribute and redirect again to your user's default application. Moreover, when initialise keycloak, you can specify to load the user profile at startup. But I don't know if you get a full user profile including custom attributes.
@dsnoeck I have already tried that during keyloak login where we pass the redirect uri. However, the redirect uri has to be specified before login, since we cannot fetch the username before user login and hence we cannot specify the redirect url since it depends on the user's custom attribute.
export class KeycloakAppAuthGuard extends KeycloakAuthGuard {
constructor(protected router: Router, protected keycloakAngular: KeycloakService) {
super(router, keycloakAngular);
}
isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
return new Promise((resolve, reject) => {
if (!this.authenticated) {
//userProfile object is undefined over here
this.keycloakAngular.login();
return;
} else {
// if authenticated redirect user
// userProfile object is available to perform the redirectUrl logic
// window.location.replace(redirectUrl);
}
resolve(true);
});
}
}
Above is my code for the custom KeycloakAuthGuard. Note, placing the redirect after the authentication will re-direct the user to the current application's root url and then re-direct the user to the mentioned redirect url.
@kasibkismath, please re-read carfully my answer. I was suggesting to always redirect to the same url after login, like /route-user. The component under this url could fetch the user attribute and redirect a 2nd time to the user's custom url
@IAmEilminx, what do you mean by session here:
Is your approach working if you need to connect with another realm within a same session ?
Do you mean an Angular session, without reloading the browser or Keycloak session ?
To be honest, I have no idea. my initializer()
method (which call keycloak.init()
) is available in my AaaService, therefor can be call from any component. But I never tried to call it again. To be tested.
@kasibkismath, please re-read carfully my answer. I was suggesting to always redirect to the same url after login, like /route-user. The component under this url could fetch the user attribute and redirect a 2nd time to the user's custom url
@dsnoeck thanks and what I did was that I introduced a component which is a loader and thus re-directing it to the URL from there.
@kasibkismath, excuse me. It is me that didn't read carefully your answer ! Thanks for sharing your solution !
@kasibkismath , @dsnoeck
I have a requirement to implement login for multi- tenants, Following is the flow of my application
Issue: I am not initializing the keycloak instance during app initialization and the initialization is done within the component that handles my tenant/realm selection.
Also my environment.ts file has no keycloak configuration, instead keyclaok configuration is done within the component that handles my tenant/realm selection.
Even though redirection is happening after login, I am not receiving any token.
Could you please provide some information or any inputs if I need to change something in implementation.
Hi @LekhaPai,
init()
method return a Promise<boolean>
, so no token. But you can use the getToken()
if you need the token.
See source code: https://github.com/mauriciovigolo/keycloak-angular/blob/master/projects/keycloak-angular/src/lib/core/services/keycloak.service.ts#L494
Please provide more informations about what you want to achieve and why if this answer doesn't help you.
Hi @dsnoeck
when I call getToken() , I receive this : ZoneAwarePromise {zone_symbolstate: null, zone_symbolvalue: Array(0)} zone_symbolstate: null zone_symbolvalue: [] proto: Object
With an error:Error: Uncaught (in promise): TypeError: Cannot read property 'login' of undefined
I want to initialize keycloak.init( ) method only when I select the realm from my home page(Home page has multiple realms options) and not during app initialisation. How can I achieve it?
Thanks in advance
Not sure to understand your problem:
Even though redirection is happening after login, I am not receiving any token.
Please provide more source code or https://plnkr.co example with clear explanation of your problem (what happen, when, how, etc ...)
Hi @dsnoeck ,
The realm-selector is the first page my application loads. realm-selector-component.ts
constructor(keycloakService:KeycloakService){}
onSelect(realm: RealmData): void {
const keycloakConfig: KeycloakConfig = {
url: 'https://example.com/auth/',
realm: realm.name,
clientId: 'test',
};
this.keycloakService.init({
config: keycloakConfig,
initOptions: {onLoad: 'login-required'},
enableBearerInterceptor: true,
});
On succesful login, I am redirected to dashboard:
dashboard.component.ts
constructor(public keycloakService: KeycloakService){}
ngOnInit(): void {
if(this.keycloakService.isLoggedIn()){
console.log('Logged in',this.keycloakService.getToken());
}
}
Also when I check firebug,
on Login, I receive the following status code in Network: Status Code: 302 Found
Hi @dsnoeck ,
The realm-selector is the first page my application loads. realm-selector-component.ts
constructor(keycloakService:KeycloakService){} onSelect(realm: RealmData): void { const keycloakConfig: KeycloakConfig = { url: 'https://example.com/auth/', realm: realm.name, clientId: 'test', }; this.keycloakService.init({ config: keycloakConfig, initOptions: {onLoad: 'login-required'}, enableBearerInterceptor: true, });
On succesful login, I am redirected to dashboard:
dashboard.component.ts
constructor(public keycloakService: KeycloakService){} ngOnInit(): void { if(this.keycloakService.isLoggedIn()){ console.log('Logged in',this.keycloakService.getToken()); } }
Also when I check firebug,
on Login, I receive the following status code in Network: Status Code: 302 Found
- Yes I am logged In successfully
- I need token, but I receive the status code as 302
@LekhaPai If you check the keycloak-angular code they are fetching the this._instance.token wrapped in a promise with await, I'm not very sure why are we not getting the token and didn't dig deeper.
However, you can get the token in an alternative way by changing your dashboard-component.ts as below.
constructor(public keycloakService: KeycloakService){}
ngOnInit(): void {
if(this.keycloakService.isLoggedIn()){
console.log('Logged in', this.keycloakService._instance.token);
}
}
Hi @kasibkismath
I tried to change in dashboard-component.ts, this is the error I am getting when trying to access token TypeError: Cannot read property 'token' of undefined.
Am I missing anything during keycloak initilisation in realm-selector-component.ts ?
Do you think my approach of keycloak initialisation is correct?
Can anyone help me with working example for multi-tenants keycloak initialization, because I need to initialize keycloak only when respective realm is selected.
@LekhaPai Please check in your browser developer console what is the error being thrown and check why is the _instance is being undefined
Hi, @kasibkismath the problem me an @LekhaPai are having is that the initial KeyCloak init Promise never gets fullfilled I think.
On startup we are redirect to localhost:8080/realms. What we are doing is by clicking an icon we start the initialization of keycloak with the specified realm, after logging in on the keycloak we are redirected to the localhost:8080, which then redirects us directly to /dashboard. It seams like the keycloak promise is not resolved yet, though when we are trying to get the token in dash-board.component.ts the object is still undefined.
What would be the correct approach for redirection or do you think it is a different issue?
I found an other approach to support multi-tenant. I'm adding the tenant in the url www.my-application.com/tenant-name Then, extract it using a regex. The regex is defined in my environment variable, therefor I can change it more easily. I also added an "offline" mode, sometime usefull in development. Note that I use the term
realm
which is corresponding to tenant in Keycloak glossary. my environment.ts:export const environment = { production: false, offline: false, defaultRealm: 'my-default-realm', multiTenant: true, realmRegExp: '^https?:\\/\\/[^\\/]+\\/([-a-z0-9]+)', baseUrl: '/', };
app-init.ts:
import { KeycloakService } from 'keycloak-angular'; import { environment } from '../../environments/environment'; export function initializer(keycloak: KeycloakService): () => Promise<any> { // Check offline mode if (environment.offline) { return (): Promise<any> => Promise.resolve(); } // Set default realm let realm = environment.defaultRealm; // Check multi-tenant if (environment.multiTenant) { const matches: RegExpExecArray = new RegExp(environment.realmRegExp).exec(window.location.href); if (matches) { realm = matches[1]; console.log('Realm found', realm); // Update the <base> href attribute document.getElementsByTagName('base').item(0).attributes.getNamedItem('href').value = environment.baseUrl + realm + '/'; } else { // Here you can redirect user to an error page. console.error('Realm not found'); return; } } return (): Promise<any> => keycloak.init({ config: { url: 'http://localhost:8080/auth', realm: realm, clientId: '<keycloak-client-id>' }, initOptions: { onLoad: 'login-required', checkLoginIframe: false } }); }
Hey @dsnoeck, I have few questions on your approach, I recently started using Key Cloak for my application, and I am really doubtful in Implementing via Angular, I have done a single tenant key cloak login, but however, I need a multi Tenant approach. Below is the configuration which I am using for keycloak integration. I didnt understand how you are able change the ip address of your instance. Are you talking about a dummy dashboard which will have realm as param? and then how am i going to get my realm name from the url, and then redirect to keycloak login dashboard. Please help
let keycloakConfig: KeycloakConfig = { url: 'http://my-ip-address/auth', realm: 'my-realm-name', clientId: 'camunda-identity-service', credentials: { secret: 'secret-key' } };
Hi @satyaram413,
We don't have a dashboard for the user to select their tenant. We simple give the user the URL, with the tenant name. So user can access it by either: http://my-secure-application/tenant-a/ or http://my-secure-application/tenant-b/
To retrieve the tenant name, we have a regex:
new RegExp(environment.realmRegExp).exec(window.location.href);
Once you have the tenant/realm name, you can execute the keycloak.init
which will redirect automatically the user to the keycloak login screen.
Hey @dsnoeck I am worried, How am i gonna test that in my local, I run that via localhost:4200, do you know any approach which i can test locally? I mean when launch my application, This is what I am thinking to do, correct me if I am wrong, or can be implemented in a better way. When I launch my application for the first time, localhost:4200, should be redirected to localhost:4200/tenant-name, this would be a different component, where in tenant-name component, i will use APP_Initiliazer to get the realm name, if there exists a realm-name, then Authguard will redirect to keycloak login page. Is this approach correct, or do i need to correct myself. -Thanks In Advance
Hi @dsnoeck, I made some progress in doing the above using your way, I deployed the dist folder, in my tomcat serve, however when I am trying to access, the page through Tomcat, i am getting redirected to keycloak, where it says, we're sorry. my url is localhost:4200. Is it because my url doesn't match with the regexp pattern?
@satyaram413, Before running the keycloak.init() you could add log to check which realm your regex retrive. Otherwise, It could be a bad keycloak configuration, like the "Allowed redirect URL" in the client configuration.
Hi @dsnoeck , I have a situation, I am trying to logout using keycloakService.logout(), session gets successfully expires, but when it redirects to this url: http://35.189.24.204:9092/auth/realms/hpi/protocol/openid-connect/auth?client_id=camunda-identity-service&redirect_uri=http%3A%2F%2Flocalhost%3A4200%2F%23%2Fmaintenance-supervisor%2Fcorrective-maintenance&state=c6189b34-dedc-4085-bedb-4e7c1c5fb613&response_mode=fragment&response_type=code&scope=openid&nonce=2d86d7b9-459a-437d-8e5d-bc54291385f1
If you observe there is a redirect uri, here so whenever i try to login again, i am going to the same page again. How can i remove redirect_uri on successful logout. Also I have assigned Valid redirect url to be * in my keycloak client window. Please help
The logout method accept a optional parameter, redirectUri. So you can define where to redirect your user: https://github.com/mauriciovigolo/keycloak-angular/blob/master/projects/keycloak-angular/src/lib/core/services/keycloak.service.ts#L284
Hi, @dsnoeck, Do you have any idea, on how i can send additional parameters in request headers, something like my plant name and tenant name. Since I am using Keycloak native interceptor to send access_token, how can i find a way to send other values.
@satyaram413, can you elaborate more what you try to do and how you do it ? Which request are you talking about ?
Hi guys, I have a problem similar to the one discussed here. This is the flow of my application
App module provider:
providers: [ { provide: HTTP_INTERCEPTORS, useClass: TokenInterceptorService, multi: true },
Routing module:
const routes: Routes = [ { path: 'realm', component: RealmSelectionComponent, }, { path: '', component: PrivateLayoutComponent, canActivate: [AuthGuard], children: [ { path: '', pathMatch: 'full', redirectTo: '/dashboard' }, { path: 'dashboard', component: DashboardComponent },
Intercepter: `export class TokenInterceptorService {
constructor(private authService: AuthService) {}
intercept(req: HttpRequest
public getBearerToken() { return 'Bearer ' + this.keycloakService.getKeycloakInstance().token; }
Auth Guard:
`export class AuthGuard extends KeycloakAuthGuard { constructor(protected router: Router, protected keycloakService: KeycloakService) { super(router, keycloakService); }
isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise
const requiredRoles = route.data.roles;
if (!requiredRoles || requiredRoles.length === 0) {
return resolve(true);
} else {
if (!this.roles || this.roles.length === 0) {
console.log('no role');
resolve(false);
}
let granted = false;
for (const requiredRole of requiredRoles) {
if (this.roles.indexOf(requiredRole) > -1) {
granted = true;
break;
}
}
resolve(granted);
}
});
} } `
Realm Selection Component:
public auth() { const realm = this.selectedRealm.charAt(0).toLocaleLowerCase() + this.selectedRealm.slice(1); this.keycloakService.init({ config: config enableBearerInterceptor: true, loadUserProfileAtStartUp: false, bearerExcludedUrls: [] }).then(); this.keycloakService.login().then(); }
I'm getting this error : ERROR Error: Uncaught (in promise): An error happened during access validation. Details:TypeError: Cannot read property 'resourceAccess' of undefined
Even if i specify the path 'localhost/realm' the page is not redirected to the dashboard.
Am I missing anything or might the application flow be incorrect?
Could you provide some guidance on this implementation, pls?
Thank you.
@mauriciovigolo did you have time to put together an example using your suggested module approach, or can you explain more about your suggested implementation?
I've been working on a POC using @dsnoeck approach and the example project. I use a custom Keycloak service, and conditionally initialise it based on the URL (window.location.href
value).
The code is available here : mabihan/keycloak-angular-multi-tenant. Hope this helps those who wanted examples on this specific use case.
Can anyone help me with working example for multi-tenants keycloak initialization, because I need to initialize keycloak only when respective realm is selected.
@LekhaPai Did you get the solution for this? If yes, could you please share it? Thanks
@PedroMPT , Did you get a solution for your question? If so can you please provide it here
Hi guys, I have a problem similar to the one discussed here. This is the flow of my application
- A page with different realms is listed
- Once the user clicks on the respective realms, keycloak is initialized with selected realm
- The user is redirected to login page of respective realm
- On successful login, redirected to dashboard
App module provider:
providers: [ { provide: HTTP_INTERCEPTORS, useClass: TokenInterceptorService, multi: true },
Routing module:
const routes: Routes = [ { path: 'realm', component: RealmSelectionComponent, }, { path: '', component: PrivateLayoutComponent, canActivate: [AuthGuard], children: [ { path: '', pathMatch: 'full', redirectTo: '/dashboard' }, { path: 'dashboard', component: DashboardComponent },
Intercepter: `export class TokenInterceptorService {
constructor(private authService: AuthService) {} intercept(req: HttpRequest, next: HttpHandler): Observable
{ const tokenizedReq = req.clone({ setHeaders: { Authorization: this.authService.getBearerToken(), } }); return next.handle(tokenizedReq); } }`
public getBearerToken() { return 'Bearer ' + this.keycloakService.getKeycloakInstance().token; }
Auth Guard:
`export class AuthGuard extends KeycloakAuthGuard { constructor(protected router: Router, protected keycloakService: KeycloakService) { super(router, keycloakService); }
isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { return new Promise((resolve, _) => { if (!this.authenticated) { this.router.navigate(['/realm']).then(); return; }
const requiredRoles = route.data.roles; if (!requiredRoles || requiredRoles.length === 0) { return resolve(true); } else { if (!this.roles || this.roles.length === 0) { console.log('no role'); resolve(false); } let granted = false; for (const requiredRole of requiredRoles) { if (this.roles.indexOf(requiredRole) > -1) { granted = true; break; } } resolve(granted); } });
} } `
Realm Selection Component:
public auth() { const realm = this.selectedRealm.charAt(0).toLocaleLowerCase() + this.selectedRealm.slice(1); this.keycloakService.init({ config: config enableBearerInterceptor: true, loadUserProfileAtStartUp: false, bearerExcludedUrls: [] }).then(); this.keycloakService.login().then(); }
I'm getting this error : ERROR Error: Uncaught (in promise): An error happened during access validation. Details:TypeError: Cannot read property 'resourceAccess' of undefined
Even if i specify the path 'localhost/realm' the page is not redirected to the dashboard.
Am I missing anything or might the application flow be incorrect?
Could you provide some guidance on this implementation, pls?
Thank you.
Bug Report or Feature Request (mark with an
x
)Versions.
Angular 7 keycloak-angular 6.0.0 keycloak 4
Desired functionality.
@mauriciovigolo i want to use keycloak-angular into multi-tenant application. This means that the realm is not known from the beggining so i can't initialize keycloak service with this information. Is there a way to initialize keycloak service during runtime of my application?