Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add remember me implementation #18768

Merged
merged 11 commits into from
Jan 29, 2024
Original file line number Diff line number Diff line change
@@ -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]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,36 @@ import { noop } from '@abp/ng.core';
import { Params } from '@angular/router';
import { from, of } from 'rxjs';
import { AuthFlowStrategy } from './auth-flow-strategy';
import { isTokenExpired } from '../utils';

export class AuthCodeFlowStrategy extends AuthFlowStrategy {
readonly isInternalAuth = false;

async init() {
this.checkRememberMeOption();

return super
.init()
.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) {
Expand All @@ -29,6 +50,7 @@ export class AuthCodeFlowStrategy extends AuthFlowStrategy {
}

logout(queryParams?: Params) {
this.rememberMeService.remove();
return from(this.oAuthService.revokeTokenAndLogout(this.getCultureParams(queryParams)));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
import { clearOAuthStorage } from '../utils/clear-o-auth-storage';
import { oAuthStorage } from '../utils/oauth-storage';
import { OAuthErrorFilterService } from '../services';
import { isTokenExpired } from '../utils';
import { RememberMeService } from '../services/remember-me.service';

export abstract class AuthFlowStrategy {
abstract readonly isInternalAuth: boolean;
Expand All @@ -35,6 +37,7 @@ export abstract class AuthFlowStrategy {
protected oAuthConfig!: AuthConfig;
protected sessionState: SessionStateService;
protected localStorageService: AbpLocalStorageService;
protected rememberMeService: RememberMeService;
protected tenantKey: string;
protected router: Router;

Expand All @@ -61,6 +64,7 @@ export abstract class AuthFlowStrategy {
this.tenantKey = injector.get(TENANT_KEY);
this.router = injector.get(Router);
this.oAuthErrorFilterService = injector.get(OAuthErrorFilterService);
this.rememberMeService = injector.get(RememberMeService);

this.listenToOauthErrors();
}
Expand All @@ -70,23 +74,20 @@ export abstract class AuthFlowStrategy {
const shouldClear = shouldStorageClear(this.oAuthConfig.clientId, oAuthStorage);
if (shouldClear) clearOAuthStorage(oAuthStorage);
}

this.oAuthService.configure(this.oAuthConfig);

this.oAuthService.events
.pipe(filter(event => event.type === 'token_refresh_error'))
.subscribe(() => this.navigateToLogin());

this.navigateToPreviousUrl();

return this.oAuthService
.loadDiscoveryDocument()
.then(() => {
if (this.oAuthService.hasValidAccessToken() || !this.oAuthService.getRefreshToken()) {
return Promise.resolve();
const isTokenExpire = isTokenExpired(this.oAuthService.getAccessTokenExpiration());
if (!isTokenExpire || this.oAuthService.getRefreshToken()) {
return this.refreshToken();
}

return this.refreshToken();
return Promise.resolve();
})
.catch(this.catchError);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,52 @@
import { filter, switchMap, tap } from 'rxjs/operators';
import { OAuthInfoEvent } from 'angular-oauth2-oidc';
import { Params, Router } from '@angular/router';
import { from, Observable, pipe } from 'rxjs';
import { from, Observable } from 'rxjs';
import { HttpHeaders } from '@angular/common/http';
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 { 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 {
readonly isInternalAuth = true;
private cookieKey = 'rememberMe';
private storageKey = 'passwordFlow';

private listenToTokenExpiration() {
this.oAuthService.events
.pipe(
filter(
event =>
event instanceof OAuthInfoEvent &&
event => event instanceof OAuthInfoEvent &&
event.type === 'token_expires' &&
event.info === 'access_token',
event.info === 'access_token'
),
)
.subscribe(() => {
if (this.oAuthService.getRefreshToken()) {
this.refreshToken();
} else {
this.oAuthService.logOut();
removeRememberMe(this.localStorageService);
this.rememberMeService.remove();
this.configState.refreshAppState().subscribe();
}
});
}

async init() {
if (!getCookieValueByName(this.cookieKey) && localStorage.getItem(this.storageKey)) {
this.oAuthService.logOut();
}
this.checkRememberMeOption();

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) {
const router = this.injector.get(Router);
return router.navigate(['/account/login'], { queryParams });
Expand All @@ -67,22 +67,23 @@ export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
),
).pipe(pipeToLogin(params, this.injector));
}

logout() {
const router = this.injector.get(Router);
const noRedirectToLogoutUrl = true;
return from(this.oAuthService.revokeTokenAndLogout(noRedirectToLogoutUrl)).pipe(
switchMap(() => this.configState.refreshAppState()),
tap(() => {
this.rememberMeService.remove();
Sinan997 marked this conversation as resolved.
Show resolved Hide resolved
router.navigateByUrl('/');
removeRememberMe(this.localStorageService);
}),
);
}

protected refreshToken() {
return this.oAuthService.refreshToken().catch(() => {
clearOAuthStorage();
removeRememberMe(this.localStorageService);
this.rememberMeService.remove();
});
}
}
27 changes: 7 additions & 20 deletions npm/ng-packs/packages/oauth/src/lib/utils/auth-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,27 @@ import {
ConfigStateService,
LoginParams,
PipeToLoginFn,
AbpLocalStorageService,
} from '@abp/ng.core';

const cookieKey = 'rememberMe';
const storageKey = 'passwordFlow';
import { RememberMeService } from '../services/remember-me.service';

export const pipeToLogin: PipeToLoginFn = function (
params: Pick<LoginParams, 'redirectUrl' | 'rememberMe'>,
injector: Injector,
) {
const configState = injector.get(ConfigStateService);
const router = injector.get(Router);
const localStorage = injector.get(AbpLocalStorageService);
const rememberMeService = injector.get(RememberMeService);
return pipe(
switchMap(() => configState.refreshAppState()),
tap(() => {
setRememberMe(params.rememberMe, localStorage);
rememberMeService.set(params.rememberMe);
if (params.redirectUrl) router.navigate([params.redirectUrl]);
}),
);
};

export function setRememberMe(
remember: boolean | undefined,
localStorageService: AbpLocalStorageService,
) {
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';
//Ref: https://github.com/manfredsteyer/angular-oauth2-oidc/issues/1214
export function isTokenExpired(expireDate: number): boolean {
const currentDate = new Date().getTime();
return expireDate < currentDate;
}
masum-ulu marked this conversation as resolved.
Show resolved Hide resolved
Loading