14

My login component briefly displays before being removed by an error message about an undefined object in a promise.

Here is the promise definition:

  static init(): Promise<any> {
    KeycloakClientService.auth.loggedIn = false;
    return new Promise((resolve, reject) => {
      const keycloakConfig = {
      url: environment.KEYCLOAK_URL,
      realm: environment.KEYCLOAK_REALM,
      clientId: environment.KEYCLOAK_CLIENTID,
      'ssl-required': 'external',
      'public-client': true
      };
      const keycloakAuth: any = new Keycloak(keycloakConfig);

      keycloakAuth.init({onLoad: 'check-sso'})
        .success(() => {
          KeycloakClientService.auth.loggedIn = true;
          KeycloakClientService.auth.authz = keycloakAuth;
          KeycloakClientService.auth.logoutUrl = environment.KEYCLOAK_URL
          + '/realms/' + environment.KEYCLOAK_REALM + '/protocol/openid-connect/logout?redirect_uri='
          + document.baseURI;
          console.log('=======>> The keycloak client has been initiated successfully');
          resolve('Succeeded in initiating the keycloak client');
        })
        .error(() => {
          reject('Failed to initiate the keycloak client');
        });
    });
  }

It is called by:

KeycloakClientService.init()
  .then(
    () => {
      console.log('The keycloak client has been initialized');
    }
  )
  .catch(
    (error) => {
      console.log(error);
      window.location.reload();
    }
  );

The console shows both messages:

The keycloak client has been initiated successfully
The keycloak client has been initialized

I'm using Angular 6.0.4 and tried following this blog

Any way to work around this error so as to keep my login form displayed ?

UPDATE: I tried using an observable instead of a promise but the issue remained the same:

  public init(): Observable<any> {
    KeycloakClientService.auth.loggedIn = false;
    return new Observable((observer) => {
      const keycloakConfig = {
        'url': environment.KEYCLOAK_URL,
        'realm': environment.KEYCLOAK_REALM,
        'clientId': environment.KEYCLOAK_CLIENTID,
        'ssl-required': 'external',
        'public-client': true
      };
      const keycloakAuth: any = new Keycloak(keycloakConfig);

      keycloakAuth.init({ 'onLoad': 'check-sso' })
        .success(() => {
          KeycloakClientService.auth.loggedIn = true;
          KeycloakClientService.auth.authz = keycloakAuth;
          KeycloakClientService.auth.logoutUrl = environment.KEYCLOAK_URL
            + '/realms/' + environment.KEYCLOAK_REALM + '/protocol/openid-connect/logout?redirect_uri='
            + document.baseURI;
          console.log('The keycloak auth has been initialized');
          observer.next('Succeeded in initiating the keycloak client');
          observer.complete();
        })
        .error(() => {
          console.log('The keycloak client could not be initiated');
          observer.error('Failed to initiate the keycloak client');
        });
    });
  }

The whole source code is available on GitHub

UPDATE: Following an answer below, I also tried to use a then() and a catch() keywords but the error remained the exact same:

keycloakAuth.init({ 'onLoad': 'check-sso' })
        .then(() => {
          KeycloakClientService.auth.loggedIn = true;
          KeycloakClientService.auth.authz = keycloakAuth;
          KeycloakClientService.auth.logoutUrl = environment.KEYCLOAK_URL
            + '/realms/' + environment.KEYCLOAK_REALM + '/protocol/openid-connect/logout?redirect_uri='
            + document.baseURI;
          console.log('The keycloak auth has been initialized');
          observer.next('Succeeded in initiating the keycloak client');
          observer.complete();
        })
        .catch(() => {
          console.log('The keycloak client could not be initiated');
          observer.error('Failed to initiate the keycloak client');
        });
Stephane
  • 11,836
  • 25
  • 112
  • 175
  • The order of the console messages appears to be wrong. How can the `keycloadClientService.init().then(...)` be called before the `resolve('Succeeded')` is called? The message with the word "successfully" is logged before the resolve function is called. Are you sure this code example is correct? – Reactgular Jun 07 '18 at 13:38
  • Also, does the `(error) => console.log(error)` catch this error? – Reactgular Jun 07 '18 at 13:39
  • @ogTag My mistake, the loggers appear indeed in the other order. I now corrected it. And no, the catch is not firing. – Stephane Jun 07 '18 at 20:52
  • Would you mind sharing your error code ? This would help a lot. –  Jun 22 '18 at 08:07
  • @trichetriche There is no such thing. The only message I got is `Uncaught (in promise): [object Undefined]`. I put the whole source code on GitHub if needed. – Stephane Jun 22 '18 at 13:46
  • And there probably is a stack trace, stating the file and line where it went wrong, isn't it ? –  Jun 22 '18 at 13:47
  • @trichetriche No, not at all. The error is swallowed by the promise. Hence my question title... – Stephane Jun 22 '18 at 16:26

3 Answers3

3

This is a wild guess, but maybe it's a conflict with Angular's zones. Since this is a security library it might not like that Angular has replaced core functions with proxies. For example, NgZone modifies window.setTimeout and the HTTP methods.

So you could try running this code outside of zones. The only problem here is that you're using a static function, and will have to make this an injectable service so that you can access NgZone

@Injectable()
export class KeycloakClientService {
    public constructor(private zone: NgZone) {
    }

    public init(): Promise<any> {
        KeycloakClientService.auth.loggedIn = false;
        return new Promise((resolve, reject) => {
            this.zone.runOutsideAngular(() => {
                const keycloakConfig = {
                     url: environment.KEYCLOAK_URL,
                     realm: environment.KEYCLOAK_REALM,
                     clientId: environment.KEYCLOAK_CLIENTID,
                     'ssl-required': 'external',
                     'public-client': true
                };

                const keycloakAuth: any = new Keycloak(keycloakConfig);

                keycloakAuth.init({onLoad: 'check-sso'})
                    .success(() => {
                        KeycloakClientService.auth.loggedIn = true;
                        KeycloakClientService.auth.authz = keycloakAuth;
                        KeycloakClientService.auth.logoutUrl = environment.KEYCLOAK_URL
                            + '/realms/' + environment.KEYCLOAK_REALM + '/protocol/openid-connect/logout?redirect_uri='
                            + document.baseURI;
                        console.log('=======>> The keycloak client has been initiated successfully');
                        resolve('Succeeded in initiating the keycloak client');
                    })
                    .error(() => {
                        reject('Failed to initiate the keycloak client');
                    });
            });
        }
    }
}

The change here is to use zone.runOutsideAngular

Reactgular
  • 52,335
  • 19
  • 158
  • 208
  • But then, how to inject the keycloakClientService into the `main.ts` file ? – Stephane Jun 07 '18 at 21:00
  • But then, do I really need to have the `KeycloakClientService.init()`call in the `main.ts` file ? Wouldn't it be better to have it in the `AppModule` or in the `AppComponent` class constructors ? – Stephane Jun 07 '18 at 21:10
  • @Stephane yes, you should put this in the AppModule constructor. Don't put in the constructor of a module that is lazy loaded. If this is something that must be done and finished before the `AppComponent` is shown, then you should put this at the top of your routes as a `Resolver`. – Reactgular Jun 07 '18 at 21:12
  • I put it in the `AppModule` constructor, and called the `ìnit()` method on an injected instance of the `keycloakClientService` service. I had the promise wrapped up in an `runOutsideAngular` block. But nothing changed, the same two loggers were called in the same order, alonside the same above errors. – Stephane Jun 07 '18 at 21:17
  • It's not possible for the success callback to be executed if that promise has an unhandled error. I think this library starts an internal promise that throws the error. It's not from your promise. You might have triggered this other promise. I don't know what to do. You need the help from people who are using this library. I don't think this is an Angular issue. – Reactgular Jun 07 '18 at 22:26
  • 1
    I sorted out my error handler and now I can say the error is being caught. I put the whole application at `https://github.com/stephaneeybert/ng-hero` if that can be of interest. – Stephane Jun 08 '18 at 06:24
  • 1
    Thanks. I'll clone it and give it a try. – Reactgular Jun 08 '18 at 12:05
  • I see it is when the `success` block of `keycloakAuth.init({onLoad: 'check-sso'})` completes that the error occurs. Removing all code in the block, even removing the `resolve()` call, doesn't change anything. It is the exiting of this `success` block that lets the error occur. – Stephane Jun 18 '18 at 21:42
  • Do you think I should log an issue at `https://issues.jboss.org/projects/KEYCLOAK/issues/` ? – Stephane Jun 19 '18 at 18:43
0

If you remove the success block, where do you run your logic within success?

I read some of their source code, I think this is why success causes the problem:

Within keycloak.js, there is a function createNativePromise():

function createNativePromise() {
var p = {
    setSuccess: function(result) {
        p.success = true;
        p.resolve(result);
    },

    setError: function(result) {
        p.success = false;
        p.reject(result);
    }
};
p.promise = new Promise(function(resolve, reject) {
    p.resolve = resolve;
    p.reject = reject;
});
p.promise.success = function(callback) {
    p.promise.then(callback);
    return p.promise;
}
p.promise.error = function(callback) {
    p.promise.catch(callback);
    return p.promise;
}
return p;
}

And it's used this way(simplified code):

function refreshToken() {
    var promise = createNativePromise();
    ...
    if (refreshTokenFailed) {
        promise.setError(true);
    }
    ...

    return promise.promise;
}

The problem is, promise.setError() calls promise.reject(result), so the promise is rejected, it's expecting a catch.

But within promise.success, there is a promise.promise.then(callback);, and nobody is catching this promise.

This is why you get the Uncaught (in promise): [object Undefined], and in my case, i always get Uncaught (in promise): true.

Solution:

Notice that promise.promise is a real Promise, so we can use then and catch instead of success and error.

The drawback is, the typescript type will be wrong.

peng37
  • 4,480
  • 1
  • 11
  • 13
  • I tried to use a `then()` and a `catch()` keywords but the error remained the exact same. I added an update to the question for more details. – Stephane Aug 10 '18 at 09:36
  • @Stephane sorry, you are right, I also still get the error. – peng37 Aug 15 '18 at 04:38
0

We have observed a similar error about the promise object undefined. The situation was our local application was working fine with the local keycloak standalone server but faced this error when the local application trying to connect with a keycloak server hosted on the ABC server (ABC is used as a reference here to give any arbitrary name).

This issue was resolved when we hosted the application and the keycloak server both on the ABC server.

It looks like there are some time sync issues in different machines due to which the promise object is not returned.

Dharman
  • 30,962
  • 25
  • 85
  • 135