Browse Source

Merge pull request #18768 from abpframework/sinan/remember_me

Add remember me implementation
pull/18891/head
İsmail ÇAĞDAŞ 3 years ago
committed by GitHub
parent
commit
c5ae1422ed
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 27
      npm/ng-packs/packages/oauth/src/lib/services/remember-me.service.ts
  2. 24
      npm/ng-packs/packages/oauth/src/lib/strategies/auth-code-flow-strategy.ts
  3. 15
      npm/ng-packs/packages/oauth/src/lib/strategies/auth-flow-strategy.ts
  4. 37
      npm/ng-packs/packages/oauth/src/lib/strategies/auth-password-flow-strategy.ts
  5. 27
      npm/ng-packs/packages/oauth/src/lib/utils/auth-utils.ts

27
npm/ng-packs/packages/oauth/src/lib/services/remember-me.service.ts

@ -0,0 +1,27 @@
import { AbpLocalStorageService } from '@abp/ng.core';
import { Injectable, inject } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class RememberMeService {
readonly #rememberMe = 'remember_me';
protected readonly localStorageService = inject(AbpLocalStorageService);
set(remember: boolean) {
this.localStorageService.setItem(this.#rememberMe, JSON.stringify(remember));
}
remove() {
this.localStorageService.removeItem(this.#rememberMe);
}
get() {
return Boolean(JSON.parse(this.localStorageService.getItem(this.#rememberMe)));
}
getFromToken(accessToken: string) {
const parsedToken = JSON.parse(atob(accessToken.split('.')[1]));
return Boolean(parsedToken[this.#rememberMe]);
}
}

24
npm/ng-packs/packages/oauth/src/lib/strategies/auth-code-flow-strategy.ts

@ -2,15 +2,36 @@ import { noop } from '@abp/ng.core';
import { Params } from '@angular/router'; import { Params } from '@angular/router';
import { from, of } from 'rxjs'; import { from, of } from 'rxjs';
import { AuthFlowStrategy } from './auth-flow-strategy'; import { AuthFlowStrategy } from './auth-flow-strategy';
import { isTokenExpired } from '../utils';
export class AuthCodeFlowStrategy extends AuthFlowStrategy { export class AuthCodeFlowStrategy extends AuthFlowStrategy {
readonly isInternalAuth = false; readonly isInternalAuth = false;
async init() { async init() {
this.checkRememberMeOption();
return super return super
.init() .init()
.then(() => this.oAuthService.tryLogin().catch(noop)) .then(() => this.oAuthService.tryLogin().catch(noop))
.then(() => this.oAuthService.setupAutomaticSilentRefresh({}, 'access_token')); .then(() => this.oAuthService.setupAutomaticSilentRefresh());
}
private checkRememberMeOption() {
const accessToken = this.oAuthService.getAccessToken();
const isTokenExpire = isTokenExpired(this.oAuthService.getAccessTokenExpiration());
let rememberMe = this.rememberMeService.get();
if (accessToken && !rememberMe) {
const rememberMeValue = this.rememberMeService.getFromToken(accessToken);
this.rememberMeService.set(!!rememberMeValue);
}
rememberMe = this.rememberMeService.get();
if (accessToken && isTokenExpire && !rememberMe) {
this.rememberMeService.remove();
this.oAuthService.logOut();
}
} }
navigateToLogin(queryParams?: Params) { navigateToLogin(queryParams?: Params) {
@ -29,6 +50,7 @@ export class AuthCodeFlowStrategy extends AuthFlowStrategy {
} }
logout(queryParams?: Params) { logout(queryParams?: Params) {
this.rememberMeService.remove();
return from(this.oAuthService.revokeTokenAndLogout(this.getCultureParams(queryParams))); return from(this.oAuthService.revokeTokenAndLogout(this.getCultureParams(queryParams)));
} }

15
npm/ng-packs/packages/oauth/src/lib/strategies/auth-flow-strategy.ts

@ -24,6 +24,8 @@ import {
import { clearOAuthStorage } from '../utils/clear-o-auth-storage'; import { clearOAuthStorage } from '../utils/clear-o-auth-storage';
import { oAuthStorage } from '../utils/oauth-storage'; import { oAuthStorage } from '../utils/oauth-storage';
import { OAuthErrorFilterService } from '../services'; import { OAuthErrorFilterService } from '../services';
import { isTokenExpired } from '../utils';
import { RememberMeService } from '../services/remember-me.service';
export abstract class AuthFlowStrategy { export abstract class AuthFlowStrategy {
abstract readonly isInternalAuth: boolean; abstract readonly isInternalAuth: boolean;
@ -35,6 +37,7 @@ export abstract class AuthFlowStrategy {
protected oAuthConfig!: AuthConfig; protected oAuthConfig!: AuthConfig;
protected sessionState: SessionStateService; protected sessionState: SessionStateService;
protected localStorageService: AbpLocalStorageService; protected localStorageService: AbpLocalStorageService;
protected rememberMeService: RememberMeService;
protected tenantKey: string; protected tenantKey: string;
protected router: Router; protected router: Router;
@ -61,6 +64,7 @@ export abstract class AuthFlowStrategy {
this.tenantKey = injector.get(TENANT_KEY); this.tenantKey = injector.get(TENANT_KEY);
this.router = injector.get(Router); this.router = injector.get(Router);
this.oAuthErrorFilterService = injector.get(OAuthErrorFilterService); this.oAuthErrorFilterService = injector.get(OAuthErrorFilterService);
this.rememberMeService = injector.get(RememberMeService);
this.listenToOauthErrors(); this.listenToOauthErrors();
} }
@ -70,23 +74,20 @@ export abstract class AuthFlowStrategy {
const shouldClear = shouldStorageClear(this.oAuthConfig.clientId, oAuthStorage); const shouldClear = shouldStorageClear(this.oAuthConfig.clientId, oAuthStorage);
if (shouldClear) clearOAuthStorage(oAuthStorage); if (shouldClear) clearOAuthStorage(oAuthStorage);
} }
this.oAuthService.configure(this.oAuthConfig); this.oAuthService.configure(this.oAuthConfig);
this.oAuthService.events this.oAuthService.events
.pipe(filter(event => event.type === 'token_refresh_error')) .pipe(filter(event => event.type === 'token_refresh_error'))
.subscribe(() => this.navigateToLogin()); .subscribe(() => this.navigateToLogin());
this.navigateToPreviousUrl(); this.navigateToPreviousUrl();
return this.oAuthService return this.oAuthService
.loadDiscoveryDocument() .loadDiscoveryDocument()
.then(() => { .then(() => {
if (this.oAuthService.hasValidAccessToken() || !this.oAuthService.getRefreshToken()) { const isTokenExpire = isTokenExpired(this.oAuthService.getAccessTokenExpiration());
return Promise.resolve(); if (!isTokenExpire || this.oAuthService.getRefreshToken()) {
return this.refreshToken();
} }
return this.refreshToken(); return Promise.resolve();
}) })
.catch(this.catchError); .catch(this.catchError);
} }

37
npm/ng-packs/packages/oauth/src/lib/strategies/auth-password-flow-strategy.ts

@ -1,31 +1,23 @@
import { filter, switchMap, tap } from 'rxjs/operators'; import { filter, switchMap, tap } from 'rxjs/operators';
import { OAuthInfoEvent } from 'angular-oauth2-oidc'; import { OAuthInfoEvent } from 'angular-oauth2-oidc';
import { Params, Router } from '@angular/router'; import { Params, Router } from '@angular/router';
import { from, Observable, pipe } from 'rxjs'; import { from, Observable } from 'rxjs';
import { HttpHeaders } from '@angular/common/http'; import { HttpHeaders } from '@angular/common/http';
import { AuthFlowStrategy } from './auth-flow-strategy'; import { AuthFlowStrategy } from './auth-flow-strategy';
import { pipeToLogin, removeRememberMe } from '../utils/auth-utils'; import { isTokenExpired, pipeToLogin } from '../utils/auth-utils';
import { LoginParams } from '@abp/ng.core'; import { LoginParams } from '@abp/ng.core';
import { clearOAuthStorage } from '../utils/clear-o-auth-storage'; import { clearOAuthStorage } from '../utils/clear-o-auth-storage';
function getCookieValueByName(name: string) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : '';
}
export class AuthPasswordFlowStrategy extends AuthFlowStrategy { export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
readonly isInternalAuth = true; readonly isInternalAuth = true;
private cookieKey = 'rememberMe';
private storageKey = 'passwordFlow';
private listenToTokenExpiration() { private listenToTokenExpiration() {
this.oAuthService.events this.oAuthService.events
.pipe( .pipe(
filter( filter(
event => event => event instanceof OAuthInfoEvent &&
event instanceof OAuthInfoEvent &&
event.type === 'token_expires' && event.type === 'token_expires' &&
event.info === 'access_token', event.info === 'access_token'
), ),
) )
.subscribe(() => { .subscribe(() => {
@ -33,20 +25,28 @@ export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
this.refreshToken(); this.refreshToken();
} else { } else {
this.oAuthService.logOut(); this.oAuthService.logOut();
removeRememberMe(this.localStorageService); this.rememberMeService.remove();
this.configState.refreshAppState().subscribe(); this.configState.refreshAppState().subscribe();
} }
}); });
} }
async init() { async init() {
if (!getCookieValueByName(this.cookieKey) && localStorage.getItem(this.storageKey)) { this.checkRememberMeOption();
this.oAuthService.logOut();
}
return super.init().then(() => this.listenToTokenExpiration()); return super.init().then(() => this.listenToTokenExpiration());
} }
private checkRememberMeOption() {
const accessToken = this.oAuthService.getAccessToken();
const isTokenExpire = isTokenExpired(this.oAuthService.getAccessTokenExpiration());
const rememberMe = this.rememberMeService.get();
if (accessToken && isTokenExpire && !rememberMe) {
this.rememberMeService.remove();
this.oAuthService.logOut();
}
}
navigateToLogin(queryParams?: Params) { navigateToLogin(queryParams?: Params) {
const router = this.injector.get(Router); const router = this.injector.get(Router);
return router.navigate(['/account/login'], { queryParams }); return router.navigate(['/account/login'], { queryParams });
@ -67,14 +67,15 @@ export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
), ),
).pipe(pipeToLogin(params, this.injector)); ).pipe(pipeToLogin(params, this.injector));
} }
logout() { logout() {
const router = this.injector.get(Router); const router = this.injector.get(Router);
const noRedirectToLogoutUrl = true; const noRedirectToLogoutUrl = true;
return from(this.oAuthService.revokeTokenAndLogout(noRedirectToLogoutUrl)).pipe( return from(this.oAuthService.revokeTokenAndLogout(noRedirectToLogoutUrl)).pipe(
switchMap(() => this.configState.refreshAppState()), switchMap(() => this.configState.refreshAppState()),
tap(() => { tap(() => {
this.rememberMeService.remove();
router.navigateByUrl('/'); router.navigateByUrl('/');
removeRememberMe(this.localStorageService);
}), }),
); );
} }
@ -82,7 +83,7 @@ export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
protected refreshToken() { protected refreshToken() {
return this.oAuthService.refreshToken().catch(() => { return this.oAuthService.refreshToken().catch(() => {
clearOAuthStorage(); clearOAuthStorage();
removeRememberMe(this.localStorageService); this.rememberMeService.remove();
}); });
} }
} }

27
npm/ng-packs/packages/oauth/src/lib/utils/auth-utils.ts

@ -6,11 +6,8 @@ import {
ConfigStateService, ConfigStateService,
LoginParams, LoginParams,
PipeToLoginFn, PipeToLoginFn,
AbpLocalStorageService,
} from '@abp/ng.core'; } from '@abp/ng.core';
import { RememberMeService } from '../services/remember-me.service';
const cookieKey = 'rememberMe';
const storageKey = 'passwordFlow';
export const pipeToLogin: PipeToLoginFn = function ( export const pipeToLogin: PipeToLoginFn = function (
params: Pick<LoginParams, 'redirectUrl' | 'rememberMe'>, params: Pick<LoginParams, 'redirectUrl' | 'rememberMe'>,
@ -18,28 +15,18 @@ export const pipeToLogin: PipeToLoginFn = function (
) { ) {
const configState = injector.get(ConfigStateService); const configState = injector.get(ConfigStateService);
const router = injector.get(Router); const router = injector.get(Router);
const localStorage = injector.get(AbpLocalStorageService); const rememberMeService = injector.get(RememberMeService);
return pipe( return pipe(
switchMap(() => configState.refreshAppState()), switchMap(() => configState.refreshAppState()),
tap(() => { tap(() => {
setRememberMe(params.rememberMe, localStorage); rememberMeService.set(params.rememberMe);
if (params.redirectUrl) router.navigate([params.redirectUrl]); if (params.redirectUrl) router.navigate([params.redirectUrl]);
}), }),
); );
}; };
export function setRememberMe( //Ref: https://github.com/manfredsteyer/angular-oauth2-oidc/issues/1214
remember: boolean | undefined, export function isTokenExpired(expireDate: number): boolean {
localStorageService: AbpLocalStorageService, const currentDate = new Date().getTime();
) { return expireDate < currentDate;
removeRememberMe(localStorageService);
localStorageService.setItem(storageKey, 'true');
document.cookie = `${cookieKey}=true; path=/${
remember ? ' ;expires=Fri, 31 Dec 9999 23:59:59 GMT' : ''
}`;
}
export function removeRememberMe(localStorageService: AbpLocalStorageService) {
localStorageService.removeItem(storageKey);
document.cookie = cookieKey + '= ; path=/; expires = Thu, 01 Jan 1970 00:00:00 GMT';
} }

Loading…
Cancel
Save