keycloak-angular icon indicating copy to clipboard operation
keycloak-angular copied to clipboard

KeycloakService initialisation in multi-tenant applications

Open nikosvr88 opened this issue 6 years ago • 37 comments

Bug Report or Feature Request (mark with an x)

- [ ] bug report -> please search for issues before submitting
- [ x] feature request

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?

nikosvr88 avatar Jan 29 '19 11:01 nikosvr88

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.

dsnoeck avatar Feb 08 '19 14:02 dsnoeck

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

dsnoeck avatar Feb 13 '19 08:02 dsnoeck

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

multi-tenant

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?

kasibkismath avatar Apr 08 '19 13:04 kasibkismath

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.

Miexil avatar Apr 17 '19 13:04 Miexil

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

  1. 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
    });
  }
  1. and in this library main app.module.ts file:
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()
    }
  ],
  ...
})
  1. and finally in my webapp app.module.ts:
@NgModule({
...
  providers: [
    {provide: 'ENV', useValue: merge(env, environment)},
    {
      provide: APP_INITIALIZER,
      useFactory(aaaService: AaaService) {
        return (): Promise<any> => {
          return aaaService.initializer();
        };
      },
      multi: true,
      deps: [AaaService]
    }
  ],
  ...
})

dsnoeck avatar Apr 17 '19 13:04 dsnoeck

@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)

Miexil avatar Apr 18 '19 09:04 Miexil

@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 avatar Apr 23 '19 08:04 kasibkismath

@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 avatar Apr 24 '19 05:04 dsnoeck

@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 avatar Apr 24 '19 06:04 kasibkismath

@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 avatar Apr 24 '19 06:04 dsnoeck

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

dsnoeck avatar Apr 24 '19 07:04 dsnoeck

@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 avatar Apr 24 '19 10:04 kasibkismath

@kasibkismath, excuse me. It is me that didn't read carefully your answer ! Thanks for sharing your solution !

dsnoeck avatar Apr 24 '19 10:04 dsnoeck

@kasibkismath , @dsnoeck

I have a requirement to implement login for multi- tenants, Following is the flow of my application

  1. A Home page with different tenants is listed
  2. Once the user clicks on the respective tenant, keycloak is initialized with selected realm 3.The user is redirected to login page of respective realm 4.On successful login, redirected to dashboard

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.

LekhaPai avatar Apr 25 '19 05:04 LekhaPai

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.

dsnoeck avatar Apr 25 '19 06:04 dsnoeck

Hi @dsnoeck

when I call getToken() , I receive this : ZoneAwarePromise {__zone_symbol__state: null, __zone_symbol__value: Array(0)} __zone_symbol__state: null __zone_symbol__value: [] 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

LekhaPai avatar Apr 25 '19 08:04 LekhaPai

Not sure to understand your problem:

Even though redirection is happening after login, I am not receiving any token.

  • Are you logged in succefully ? (check the user's sessions in keycloak)
  • Do you need the token for futher actions ?

Please provide more source code or https://plnkr.co example with clear explanation of your problem (what happen, when, how, etc ...)

dsnoeck avatar Apr 25 '19 09:04 dsnoeck

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 avatar Apr 25 '19 09:04 LekhaPai

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

kasibkismath avatar Apr 25 '19 11:04 kasibkismath

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?

LekhaPai avatar Apr 25 '19 11:04 LekhaPai

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 avatar Apr 25 '19 11:04 LekhaPai

@LekhaPai Please check in your browser developer console what is the error being thrown and check why is the _instance is being undefined

kasibkismath avatar Apr 25 '19 12:04 kasibkismath

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?

mficht avatar Apr 28 '19 18:04 mficht

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

satyaram413 avatar Oct 30 '19 11:10 satyaram413

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.

dsnoeck avatar Oct 30 '19 13:10 dsnoeck

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

satyaram413 avatar Oct 31 '19 06:10 satyaram413

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 avatar Oct 31 '19 12:10 satyaram413

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

dsnoeck avatar Nov 01 '19 09:11 dsnoeck

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

satyaram413 avatar Nov 02 '19 11:11 satyaram413

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

dsnoeck avatar Nov 04 '19 07:11 dsnoeck