Keycloak for ionic app : keycloak-js with cordova-native does not work
Asked Answered
N

4

5

I am trying to use the Keycloak-js(from 4.4.0.Final) library in my ionic(4) cordova application. I have followed the example and instructions from the documentation. I have installed cordova-plugin-browsertab, cordova-plugin-deeplinks, cordova-plugin-inappbrowser. Added <preference name="AndroidLaunchMode" value="singleTask" /> in my config.xml And this is how my modifications to config.xml looks like.

<widget id="org.phidatalab.radar_armt"....>

<plugin name="cordova-plugin-browsertab" spec="0.2.0" />
<plugin name="cordova-plugin-inappbrowser" spec="3.0.0" />
<plugin name="cordova-plugin-deeplinks" spec="1.1.0" />
<preference name="AndroidLaunchMode" value="singleTask" />
<allow-intent href="http://*/*" />
<allow-intent href="https://*/*" />
<universal-links>
    <host name="keycloak-cordova-example.exampledomain.net" scheme="https">
        <path event="keycloak" url="/login" />
    </host>
</universal-links>
</widget>

and my service which uses Keycloak-js looks like below.

static init(): Promise<any> {
  // Create a new Keycloak Client Instance
  let keycloakAuth: any = new Keycloak({
      url: 'https://exampledomain.net/auth/',
      realm: 'mighealth',
      clientId: 'armt',

  });

    return new Promise((resolve, reject) => {
      keycloakAuth.init({
          onLoad: 'login-required',
          adapter: 'cordova-native',
          responseMode: 'query',
          redirectUri: 'android-app://org.phidatalab.radar_armt/https/keycloak-cordova-example.github.io/login'
      }).success(() => {

          console.log("Success")
          resolve();
        }).error((err) => {
          reject(err);
        });
    });
  }

I can successfully build and run the application for Android. However, it doesn't work. From adb logs I get ( For both cordova and cordova-native adapters)

12-04 19:07:35.911 32578-32578/org.phidatalab.radar_armt D/SystemWebChromeClient: ng:///AuthModule/EnrolmentPageComponent.ngfactory.js: Line 457 : ERROR
12-04 19:07:35.911 32578-32578/org.phidatalab.radar_armt I/chromium: [INFO:CONSOLE(457)] "ERROR", source: ng:///AuthModule/EnrolmentPageComponent.ngfactory.js (457)
12-04 19:07:35.918 32578-32578/org.phidatalab.radar_armt D/SystemWebChromeClient: ng:///AuthModule/EnrolmentPageComponent.ngfactory.js: Line 457 : ERROR CONTEXT
12-04 19:07:35.919 32578-32578/org.phidatalab.radar_armt I/chromium: [INFO:CONSOLE(457)] "ERROR CONTEXT", source: ng:///AuthModule/EnrolmentPageComponent.ngfactory.js (457)

If I try to run it on browser, I get "universalLink is undefined".

I would really like some help to get this working. What am I missing? Any kind of help is much appreciated. Or is there a workaround/examples to get keycloak working for an ionic (public) client?

Nereidanereids answered 5/12, 2018 at 11:1 Comment(1)
Hi @Baptise, unfortunately the existing keycloak-js with cordova-native implementation does not work for ionic3 as expected. So I implemented the solution myself. I will post what I have implemented shortlyNereidanereids
N
12

I am posting my solution here, since I wasted a lot of time getting available plugin working for my environments. The implementation provided by keycloak-js is fairly outdated. So if you try to use it for an ionic-3 app, it just doesn't work.

My solution to get this working is using InAppBrowser plugin (similar to cordova approach of keycloak-js) and follow standard Oauth2 authorization_code procedure. I had a look into the code of keycloak-js and implemented solution based on it. Thanks to keycloak-js too.

Here it is. Step1: Install [cordova-inapp-browser][1].

Step2: A sample keycloak-auth.service.ts could look like below. This could potentially replace keycloak-js, but only for cordova option.

import 'rxjs/add/operator/toPromise'

import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'
import {Injectable} from '@angular/core'
import {JwtHelperService} from '@auth0/angular-jwt'
import {StorageService} from '../../../core/services/storage.service'
import {StorageKeys} from '../../../shared/enums/storage'
import {InAppBrowser, InAppBrowserOptions} from '@ionic-native/in-app-browser';


const uuidv4 = require('uuid/v4');

@Injectable()
export class AuthService {
  URI_base: 'https://my-server-location/auth';
  keycloakConfig: any;

  constructor(
    public http: HttpClient,
    public storage: StorageService,
    private jwtHelper: JwtHelperService,
    private inAppBrowser: InAppBrowser,
  ) {
      this.keycloakConfig = {
        authServerUrl: 'https://my-server-location/auth/', //keycloak-url
        realm: 'myrealmmName', //realm-id
        clientId: 'clientId', // client-id
        redirectUri: 'http://my-demo-app/callback/',  //callback-url registered for client.
                                                      // This can be anything, but should be a valid URL
      };
  }

  public keycloakLogin(login: boolean): Promise<any> {
    return new Promise((resolve, reject) => {
      const url = this.createLoginUrl(this.keycloakConfig, login);

      const options: InAppBrowserOptions = {
        zoom: 'no',
        location: 'no',
        clearsessioncache: 'yes',
        clearcache: 'yes'
      }
      const browser = this.inAppBrowser.create(url, '_blank', options);

      const listener = browser.on('loadstart').subscribe((event: any) => {
        const callback = encodeURI(event.url);
        //Check the redirect uri
        if (callback.indexOf(this.keycloakConfig.redirectUri) > -1) {
          listener.unsubscribe();
          browser.close();
          const code = this.parseUrlParamsToObject(event.url);
          this.getAccessToken(this.keycloakConfig, code).then(
            () => {
              const token = this.storage.get(StorageKeys.OAUTH_TOKENS);
              resolve(token);
            },
            () => reject("Count not login in to keycloak")
          );
        }
      });

    });
  }

  parseUrlParamsToObject(url: any) {
    const hashes = url.slice(url.indexOf('?') + 1).split('&');
    return hashes.reduce((params, hash) => {
      const [key, val] = hash.split('=');
      return Object.assign(params, {[key]: decodeURIComponent(val)})
    }, {});
  }

  createLoginUrl(keycloakConfig: any, isLogin: boolean) {
    const state = uuidv4();
    const nonce = uuidv4();
    const responseMode = 'query';
    const responseType = 'code';
    const scope = 'openid';
    return this.getUrlForAction(keycloakConfig, isLogin) +
      '?client_id=' + encodeURIComponent(keycloakConfig.clientId) +
      '&state=' + encodeURIComponent(state) +
      '&redirect_uri=' + encodeURIComponent(keycloakConfig.redirectUri) +
      '&response_mode=' + encodeURIComponent(responseMode) +
      '&response_type=' + encodeURIComponent(responseType) +
      '&scope=' + encodeURIComponent(scope) +
      '&nonce=' + encodeURIComponent(nonce);
  }

  getUrlForAction(keycloakConfig: any, isLogin: boolean) {
    return isLogin ? this.getRealmUrl(keycloakConfig) + '/protocol/openid-connect/auth'
      : this.getRealmUrl(keycloakConfig) + '/protocol/openid-connect/registrations';
  }

  loadUserInfo() {
    return this.storage.get(StorageKeys.OAUTH_TOKENS).then( tokens => {
      const url = this.getRealmUrl(this.keycloakConfig) + '/protocol/openid-connect/userinfo';
      const headers = this.getAccessHeaders(tokens.access_token, 'application/json');
      return this.http.get(url, {headers: headers}).toPromise();
    })
  }

  getAccessToken(kc: any, authorizationResponse: any) {
    const URI = this.getTokenUrl();
    const body = this.getAccessTokenParams(authorizationResponse.code, kc.clientId, kc.redirectUri);
    const headers = this.getTokenRequestHeaders();

    return this.createPostRequest(URI,  body, {
      header: headers,
    }).then((newTokens: any) => {
      newTokens.iat = (new Date().getTime() / 1000) - 10; // reduce 10 sec to for delay
      this.storage.set(StorageKeys.OAUTH_TOKENS, newTokens);
    });
  }

  refresh() {
    return this.storage.get(StorageKeys.OAUTH_TOKENS)
      .then(tokens => {
        const decoded = this.jwtHelper.decodeToken(tokens.access_token)
        if (decoded.iat + tokens.expires_in < (new Date().getTime() /1000)) {
          const URI = this.getTokenUrl();
          const headers = this.getTokenRequestHeaders();
          const body = this.getRefreshParams(tokens.refresh_token, this.keycloakConfig.clientId);
          return this.createPostRequest(URI, body, {
            headers: headers
          })
        } else {
          return tokens
        }
      })
      .then(newTokens => {
        newTokens.iat = (new Date().getTime() / 1000) - 10;
        return this.storage.set(StorageKeys.OAUTH_TOKENS, newTokens)
      })
      .catch((reason) => console.log(reason))
  }

  createPostRequest(uri, body, headers) {
    return this.http.post(uri, body, headers).toPromise()
  }

  getAccessHeaders(accessToken, contentType) {
    return new HttpHeaders()
      .set('Authorization', 'Bearer ' + accessToken)
      .set('Content-Type', contentType);
  }

  getRefreshParams(refreshToken, clientId) {
    return new HttpParams()
      .set('grant_type', 'refresh_token')
      .set('refresh_token', refreshToken)
      .set('client_id', encodeURIComponent(clientId))
  }

  getAccessTokenParams(code , clientId, redirectUrl) {
    return new HttpParams()
      .set('grant_type', 'authorization_code')
      .set('code', code)
      .set('client_id', encodeURIComponent(clientId))
      .set('redirect_uri', redirectUrl);
  }

  getTokenUrl() {
    return this.getRealmUrl(this.keycloakConfig) + '/protocol/openid-connect/token';
  }

  getTokenRequestHeaders() {
    const headers = new HttpHeaders()
      .set('Content-Type', 'application/x-www-form-urlencoded');

    const clientSecret = (this.keycloakConfig.credentials || {}).secret;
    if (this.keycloakConfig.clientId && clientSecret) {
      headers.set('Authorization', 'Basic ' + btoa(this.keycloakConfig.clientId + ':' + clientSecret));
    }
    return headers;
  }

  getRealmUrl(kc: any) {
    if (kc && kc.authServerUrl) {
      if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') {
        return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm);
      } else {
        return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm);
      }
    } else {
      return undefined;
    }
  }
}

Step 3: Then you can use this service in your components to what is necessary.

@Component({
  selector: 'page-enrolment',
  templateUrl: 'enrolment-page.component.html'
})
export class EnrolmentPageComponent {
constructor(
    public storage: StorageService,
    private authService: AuthService,
  ) {}
  goToRegistration() {
    this.loading = true;
    this.authService.keycloakLogin(false)
      .then(() => {
        return this.authService.retrieveUserInformation(this.language)
      });
  }
}

Note: keycloakLogin(true) takes you to login page or keycloakLogin(false) takes you to registration page of keycloak.

I hope this helps you solve it more or less.

Nereidanereids answered 25/1, 2019 at 18:29 Comment(7)
I haven't tried that yet. I have only ran it on AndroidNereidanereids
Thanks. I also spent a ton of time around the standard implementation of keycloak JS and it didnt work properlyDoerrer
I can imagine. I spent a week. Happy to hear it helpsNereidanereids
@Ketu, I am storing the access token on my phones localstorage with other data and i perform logout through app's interface. When I logout, I clear the existing token. In addition you can also try calling the logout url like this https://mcmap.net/q/295286/-logout-user-via-keycloak-rest-api-doesn-39-t-workNereidanereids
@NehaM, i tried the mentioned link, however in my case the logout works intermittently.Seal
@Nereidanereids where did you get the import StorageKeys from? I cannot find anything regarding this import and not even google can help me.Mediant
@Mediant Please ignore the storageKeys. it is an internal classNereidanereids
P
2

For those having a similar issue, I've written a library about it, which only works on Android and IOS platforms, to make the login using Keycloak. This should work fine.

This also maintains the user session. Just give it a try.

@cmotion/ionic-keycloak-auth

Palliate answered 25/9, 2019 at 21:7 Comment(1)
Thanks for this library. Authentication works but I run into CORS issues using HttpClient from Angular in my native Android app. I can avoid those by using HTTP from @ionic-native/http/ngx. But then, the interceptor does not work (of course). How did you work around that?Timorous
N
0

I recently faced a similar issue with my vanilla Cordova app. I was able to redirect the user to Keycloak login page, but nothing happened after coming back to Cordova app. My init code looks like below,

keycloak.init({
      onLoad: "check-sso",
      adapter: 'cordova-native',
      redirectUri: 'https://mywebsite.for-universal.link',
      checkLoginIframe : false
    });

I concluded that Deeplink was working fine as my app was opened after redirect to deeplink url. But somehow adapter was not able to detect the parameters sent back to app along with deeplink url.

When I check the keycloak.js adapter implementation, I got to know that keycloak specifically expects "keycloak" event to be triggered from deeplink plugin.

So, I added event name in universal link config,

<universal-links>
    <host name="mywebsite.for-universal.link" scheme="https" event="keycloak">
            <path url="*" />
    </host>
</universal-links>

It works fine now. Hope it helps someone.

Nicol answered 6/5 at 7:10 Comment(0)
H
-1

The problem with this API is that there is no way to configure the routes using the capacitor.

Hyper answered 11/9, 2020 at 15:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.