wi3land / ionic-appauth

Intergration for OpenId/AppAuth-JS into Ionic V3/4/5
MIT License
98 stars 73 forks source link

I have issue with refreshtoken , Two requests at the same time #200

Open Moataz01210049831 opened 1 year ago

Moataz01210049831 commented 1 year ago

Two requests happened for getting refresh token first one success but second failed and log me out it's failed due to taking the same exact refresh token in payload not new one here my interciptor code

/ eslint-disable @typescript-eslint/naming-convention / import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { finalize } from 'rxjs/operators'; import { environment } from 'src/environments/environment';

import { TokenResponse, Requestor } from '@openid/appauth'; import { AuthService } from 'ionic-appauth'; import { from, Observable } from 'rxjs';

@Injectable() export class AuthInterceptor implements HttpInterceptor { private secureRoutes = environment.secureExternalApis; private secureExternalRoutes = environment.secureExternalRoutes; private guestToken = environment.guestToken; constructor(private authService: AuthService, private appState: Store, public store: Store,

) { }

private onEnd(): void { console.log("we end here ") }

intercept(req: HttpRequest, next: HttpHandler): Observable<HttpEvent> { // convert promise to observable using 'from' operator return from(this.handle(req, next)) }

async handle(request: HttpRequest, next: HttpHandler): Promise<HttpEvent> { const lang = localStorage.getItem('culture') ?? 'ar'; if ( !this.secureRoutes.find((x) => request.url.startsWith(x)) || this.guestToken.find((x) => request.url.includes(x)) // && request.withCredentials !== true ) { if (this.secureExternalRoutes.find((x) => request.url.includes(x))) {

    const token: TokenResponse = await this.authService.getValidToken();
    // debugger;
    request = request.clone({
      setHeaders: {
        // authorization: `Bearer ${t}`,
        authorization: `${(token.tokenType === 'bearer') ? 'Bearer' : token.tokenType} ${token.accessToken}`,
        'Accept-Language': lang,
      },
    });
    return next.handle(request).toPromise();;

  }
  else if (this.guestToken.find((x) => request.url.includes(x))) {
    // let token: TokenResponse = await this.getToken();

    // if (token && token.accessToken) {
    //   request = request.clone({
    //     setHeaders: {
    //       authorization: `${(token.tokenType === 'bearer') ? 'Bearer' : token.tokenType} ${token.accessToken}`,
    //       'Accept-Language': lang,
    //     },
    //   });
    // }
    // else {
    let guestUserToken: any = JSON.parse(localStorage.getItem('guestUserToken'));
    request = request.clone({
      setHeaders: {
        authorization: `Bearer ${guestUserToken}`,
        //  authorization: `${(guestUserToken.token_type === 'bearer') ? 'Bearer' : guestUserToken.token_type} ${guestUserToken.access_token}`,
        'Accept-Language': lang,
      },
    });
    //}

    return next.handle(request).toPromise();;

  }

  else {
    request = request.clone({
      setHeaders: {
        'Accept-Language': lang,
      },
    });
    return next.handle(request).toPromise();;

  }
}

let token: TokenResponse = await this.getToken();

if (!token) {
  request = request.clone({
    setHeaders: {
      'Accept-Language': lang,
    },
  });
  return next.handle(request).pipe().toPromise();;
}
request = request.clone({
  setHeaders: {
    // authorization: `Bearer ${token}`,
    authorization: `${(token.tokenType === 'bearer') ? 'Bearer' : token.tokenType} ${token.accessToken}`,
    'Accept-Language': lang,
  },
});

return next.handle(request).pipe().toPromise();

}

async getToken() { let tokenResponse: TokenResponse = await this.authService.getValidToken(10);

return tokenResponse;

}

getRequest(request: HttpRequest, lang, token) { if (token && token.accessToken) { request = request.clone({ setHeaders: { authorization: ${(token.tokenType === 'bearer') ? 'Bearer' : token.tokenType} ${token.accessToken}, 'Accept-Language': lang, }, }); } else { request = request.clone({ setHeaders: { 'Accept-Language': lang, }, }); } }

}

tomek14 commented 1 year ago

Hi, I am also having the same problem - when trying to refresh token I got 2 calls, one of them is failing because the old refresh token gets revoked by the call that will first reach Token Endpoint. It doesn't matter if the access token was expired or not, I just called refreshToken() method from to check it out.

One thing that I noticed is that when I am using Android Emulator with Pixel 4 and Android 11, everything works fine, but when I am using Galaxy S21 with Android 13 or Nokia T20 Tablet with Android 12 the problem occurs. I don't have an iPhone or Mac to check it out.

Strange thing because everything was running fine until now, maybe this is a problem with Android update.

BTW. I am using Angular 13 with Ionic 6 and Capacitor 4.8. I also upgraded ionic-appauth to 2.0.0 to check if something changed, but no success.

Moataz01210049831 commented 1 year ago

@tomek14 I edited in service itself trying to run it only once and it worked fine, but if i closed app and opened again user have to login again

richardkshergold commented 1 year ago

@Moataz01210049831 I'm a bit confused by what you are doing - what is your guestToken ? In my httpinterceptor all I do is call the library's getValldToken method and use the token that comes back ? As far as I'm concerned the library should take care of everything else. Are you overcomplicating things or am I under-complicating things?

Moataz01210049831 commented 1 year ago

Yes you are right it's suppose to solve everything , but when i didn't found any response to my problem i just add a small change in refreshToken function , to make it work for one time only

richardkshergold commented 1 year ago

@Moataz01210049831 ok thanks - what small change did you make?

Moataz01210049831 commented 1 year ago

public async refreshToken() { if(localStorage.getItem('refreshToken')!==null){ return null; } localStorage.setItem('refreshToken', new Date().toLocaleTimeString()); await this.requestTokenRefresh().then(()=>{ localStorage.removeItem('refreshToken'); }).catch((response) => {

        this.storage.removeItem(TOKEN_RESPONSE_KEY);

        this.notifyActionListers(AuthActionBuilder.RefreshFailed(response));
    });
}

i did this in auth-service.ts

tomek14 commented 1 year ago

I think I solved the doubled TokenRefresh request issue. It seems that when the application uses newer WebViews, the deprecated .toPromise() method from RxJs works incorrectly. I solved it by applying fixes from RxJS docs https://rxjs.dev/deprecations/to-promise:

import { Injectable } from '@angular/core';
import { Requestor } from '@openid/appauth';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { XhrSettings } from 'ionic-appauth/lib/cordova';
import { firstValueFrom } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class NgHttpService implements Requestor {

  constructor(private http: HttpClient) {
  }

  public async xhr<T>(settings: XhrSettings): Promise<T> {
    if (!settings.method) {
      settings.method = 'GET';
    }

    switch (settings.method) {
      case 'GET':
        return firstValueFrom(this.http.get<T>(settings.url, { headers: this.getHeaders(settings.headers) }));
      case 'POST':
        return firstValueFrom(this.http.post<T>(settings.url, settings.data, { headers: this.getHeaders(settings.headers) }));
      case 'PUT':
        return firstValueFrom(this.http.put<T>(settings.url, settings.data, { headers: this.getHeaders(settings.headers) }));
      case 'DELETE':
        return firstValueFrom(this.http.delete<T>(settings.url, { headers: this.getHeaders(settings.headers) }));
    }
  }

  private getHeaders(headers: any): HttpHeaders {
    let httpHeaders: HttpHeaders = new HttpHeaders();

    if (headers !== undefined) {
      Object.keys(headers).forEach((key) => {
        httpHeaders = httpHeaders.append(key, headers[key]);
      });
    }

    return httpHeaders;
  }
}

You can also switch to Capacitor Http native implementation, and I personally switched to this implementation:

import { Injectable } from '@angular/core';
import { Requestor } from '@openid/appauth';
import { CapacitorHttp, HttpResponse  } from '@capacitor/core';
import { XhrSettings } from 'ionic-appauth/lib/cordova';

@Injectable({
  providedIn: 'root',
})
export class CapacitorHttpService implements Requestor {
  public async xhr<T>(settings: XhrSettings): Promise<T> {
    if (!settings.method) {
      settings.method = 'GET';
    }

    const response: HttpResponse = await CapacitorHttp.request({
      method: settings.method,
      url: settings.url,
      headers: settings.headers,
      data: settings.data,
    });
    return response.data as T;
  }
}

Please, try this fix and let me know if this was also your case.

Also, after this I encountered second issue. When request from loadUserInfo() method ends with 401 Http error code, boradcasted event is still LoadUserInfoSuccess instead of LoadUserInfoFailed.