Skip to content

Commit

Permalink
Merge pull request #18768 from abpframework/sinan/remember_me
Browse files Browse the repository at this point in the history
Add remember me implementation
  • Loading branch information
ismcagdas authored Jan 29, 2024
2 parents d8f533b + 0b63b88 commit c5ae142
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 46 deletions.
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();
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;
}

0 comments on commit c5ae142

Please sign in to comment.