Skip to content

Instantly share code, notes, and snippets.

@ptesser
Last active September 17, 2019 21:07
Show Gist options
  • Save ptesser/f859f8aaa5d37bba86856b3f5776e97a to your computer and use it in GitHub Desktop.
Save ptesser/f859f8aaa5d37bba86856b3f5776e97a to your computer and use it in GitHub Desktop.
Refresh Token interceptor and authentication service used to store local user information in an Ionic 3 project with the Pub/Sub pattern implemented with RxJS BehaviorSubjet.
const X_WWW_FORM_URLENCODED_OPTIONS = {
headers: new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
};
export const USER_KEY = 'user';
export const USERNAME_KEY = 'username';
export const TOKEN_KEY = 'token';
export const INSTITUTION_KEY = 'institution';
@Injectable()
export class AuthService {
readonly token$ = new BehaviorSubject<Token | undefined>(undefined);
readonly user$ = new BehaviorSubject<InfoUtente | undefined>(undefined);
readonly username$ = new BehaviorSubject<string | undefined>(undefined);
readonly institutions$ = new BehaviorSubject<InfoEnte[]>([]);
clientId = 'test|fake-serial';
get accessToken() {
return this.token$.value && this.token$.value.access_token;
}
get refreshToken() {
return this.token$.value && this.token$.value.refresh_token;
}
get username() {
return this.username$.value;
}
get isAuthenticate() {
return !!this.accessToken;
}
get isAuthenticate$() {
return this.storage.get(TOKEN_KEY)
.then((token: Token) => !!(token && token.access_token));
}
get isConsentAllow$() {
return this.storage.get(USER_KEY)
.then((user: InfoUtente) => !!(user && (user.consenso01 && user.consenso02)));
}
constructor(
@Optional() private readonly configuration: Configuration,
private readonly http: HttpClient,
private readonly userService: UtenteService,
private readonly device: Device,
private readonly storage: Storage,
private readonly platform: Platform,
) {
this.setClientId();
this.extractDataFromLocaleStorage();
}
login(request?: LoginRequest) {
const fakeRequest: LoginRequest = {
...request,
grant_type: 'password',
client_id: this.clientId,
};
const loginEncodedRequest = this.configuration.getFormEncodedRequest(fakeRequest);
// FIXME: Get endpoint from Swagger doc, injected
return this.http.post('https://service.pmpay.it/pagopapp/auth', loginEncodedRequest, X_WWW_FORM_URLENCODED_OPTIONS)
.pipe(
tap((token: Token) => this.setToken(token)),
switchMap(() => this.userService.infoSessione().pipe(
tap((res) => this.setInfo(res)),
tap(() => this.setUsername(request.username)),
)),
);
}
logout() {
const revokeRequest: LoginRequest = {
client_id: this.clientId || 'test|xxx',
token: this.accessToken,
};
const revokeEncodedRequest = this.configuration.getFormEncodedRequest(revokeRequest);
if (this.configuration.accessToken) {
const accessToken = typeof this.configuration.accessToken === 'function'
? this.configuration.accessToken()
: this.configuration.accessToken;
X_WWW_FORM_URLENCODED_OPTIONS.headers = X_WWW_FORM_URLENCODED_OPTIONS.headers.set('Authorization', 'Bearer ' + accessToken);
}
this.storage.clear().then(() => {
this.clearData();
});
return this.http.post('https://service.pmpay.it/pagopapp/dauth', revokeEncodedRequest, X_WWW_FORM_URLENCODED_OPTIONS);
}
refresh() {
if (!this.refreshToken) {
this.logout().subscribe();
return undefined;
}
const refreshRequest: LoginRequest = {
client_id: this.clientId,
grant_type: 'refresh_token',
refresh_token: this.refreshToken,
};
const refreshEncodedRequest = this.configuration.getFormEncodedRequest(refreshRequest);
return this.http.post('https://service.pmpay.it/pagopapp/rauth', refreshEncodedRequest, X_WWW_FORM_URLENCODED_OPTIONS)
.pipe(
tap((token: Token) => this.setToken(token)),
);
}
consent(request: RichiestaConsenso) {
return this.userService.consenso(request).pipe(
switchMap(() => {
return request.consenso01 || request.consenso02
? this.userService.infoSessione().pipe(tap((res) => this.setInfo(res)))
: this.userService.infoSessione().pipe(
tap((res) => this.setInfo(res)),
switchMap(() => Observable.throw('Cannot navigate with falsy values')),
);
}),
);
}
proof(request: RichiestaProva) {
return this.userService.prova(request).pipe(
switchMap(() => {
return this.userService.infoSessione().pipe(tap((res) => this.setInfo(res)))
}),
);
}
private setUsername(username: string) {
this.username$.next(username)
this.storage.set(USERNAME_KEY, username);
}
private setInfo(info: RispostaInfoSessione) {
this.user$.next(info.infoUtente);
this.institutions$.next(info.infoEnti);
this.storage.set(USER_KEY, info.infoUtente);
this.storage.set(INSTITUTION_KEY, info.infoEnti);
}
private setToken(token: Token) {
this.token$.next(token);
this.storage.set(TOKEN_KEY, token);
// set token inside Swagger object used to set token to generated APIs
if (this.configuration) {
this.configuration.accessToken = token.access_token;
}
}
private setClientId(value?: string) {
if (value) {
this.clientId = value;
}
this.platform.ready().then(() => {
if (this.device.serial) {
this.clientId = `test|${this.device.serial}`;
}
})
}
private extractDataFromLocaleStorage() {
this.storage.get(USER_KEY).then((val: InfoUtente) => {
if (val) {
this.user$.next(val);
}
});
this.storage.get(USERNAME_KEY).then((val: string) => {
if (val) {
this.username$.next(val);
}
});
this.storage.get(TOKEN_KEY).then((val: Token) => {
if (val) {
this.token$.next(val);
if (this.configuration) {
this.configuration.accessToken = val.access_token;
}
}
});
this.storage.get(INSTITUTION_KEY).then((val: InfoEnte[]) => {
if (val) {
this.institutions$.next(val);
}
});
}
private clearData() {
this.token$.next(undefined);
this.user$.next(undefined);
this.username$.next(undefined);
this.institutions$.next([]);
if (this.configuration) {
this.configuration.accessToken = null;
}
}
}
@Injectable()
export class RefreshTokenInterceptor implements HttpInterceptor {
private refreshTokenInProgress = false;
private readonly refreshTokenSubject = new BehaviorSubject<string | null>(null);
constructor(
private readonly authService: AuthService,
) {
}
// Intercepts all HTTP requests!
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
catchError((error) => {
if (request.url.includes('rauth') || request.url.includes('auth')) {
if (request.url.includes('rauth')) {
this.authService.logout().subscribe();
}
return Observable.throw(error);
}
if (error.status !== 401) {
return Observable.throw(error);
}
if (this.refreshTokenInProgress) {
// If refreshTokenInProgress is true, we will wait until refreshTokenSubject has a non-null value
// – which means the new token is ready and we can retry the request again
return this.refreshTokenSubject.pipe(
filter(result => !!result),
take(1),
switchMap(() => next.handle(this.addAuthenticationToken(request))),
);
} else {
this.refreshTokenInProgress = true;
// // Set the refreshTokenSubject to null so that subsequent API calls will wait until the new token has been retrieved
this.refreshTokenSubject.next(null);
// // Call auth.refreshAccessToken(this is an Observable that will be returned)
return this.authService.refresh().pipe(
switchMap((token: Token) => {
//When the call to refreshToken completes we reset the refreshTokenInProgress to false
// for the next time the token needs to be refreshed
this.refreshTokenInProgress = false;
this.refreshTokenSubject.next(token.refresh_token);
return next.handle(this.addAuthenticationToken(request));
}),
catchError((err) => {
this.refreshTokenInProgress = false;
if (err.url.includes('rauth')) {
this.authService.logout().subscribe();
}
return Observable.throw(error);
}),
)
}
})
);
}
private addAuthenticationToken(request: HttpRequest<any>) {
// Get access token from Local Storage
const accessToken = this.authService.accessToken;
// If access token is null this means that user is not logged in
// And we return the original request
if (!accessToken) {
return request;
}
// We clone the request, because the original request is immutable
return request.clone({
setHeaders: {
Authorization: `Bearer ${this.authService.accessToken}`,
}
});
}
}
@ptesser
Copy link
Author

ptesser commented Mar 6, 2019

AuthService have some properties and methods used to trigger API calls and get access to some information.

@ptesser
Copy link
Author

ptesser commented Mar 6, 2019

AuthService is related to specific project models and business logic, but shows some general implementation for OAuth flow authentication

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment