🐛Do not send "/login" request multiple times on application init (#149)

This commit is contained in:
Anton Shepilov
2023-07-17 16:41:37 +02:00
committed by GitHub
parent 6cbdc9d0df
commit 28ec7c1084
10 changed files with 148 additions and 110 deletions
@@ -40,7 +40,7 @@ export default () => {
centered centered
closable={false} closable={false}
title={null} title={null}
visible={open} open={open}
footer={null} footer={null}
destroyOnClose={true} destroyOnClose={true}
width={ModalManager.getPosition()?.size?.width || '700px'} width={ModalManager.getPosition()?.size?.width || '700px'}
@@ -50,6 +50,7 @@ class AuthService {
const config = InitService.server_infos?.configuration?.accounts[accountType]; const config = InitService.server_infos?.configuration?.accounts[accountType];
console.log(`Use "${Globals.environment.env_dev_auth}" account type for authorization`)
if (Globals.environment.env_dev_auth) accountType = Globals.environment.env_dev_auth; if (Globals.environment.env_dev_auth) accountType = Globals.environment.env_dev_auth;
if (accountType === 'remote') { if (accountType === 'remote') {
@@ -104,7 +105,6 @@ class AuthService {
onSessionExpired: () => this.onSessionExpired(), onSessionExpired: () => this.onSessionExpired(),
onNewToken: async token => { onNewToken: async token => {
this.onNewToken(token); this.onNewToken(token);
// TODO: Change the basic auth to return this new token on init // TODO: Change the basic auth to return this new token on init
if (this.initState === 'initializing') { if (this.initState === 'initializing') {
const user = await this.comleteInit(); const user = await this.comleteInit();
@@ -118,6 +118,8 @@ class AuthService {
resolve(null); resolve(null);
}, },
}); });
this.logger.info("Init completed")
resolve(null);
}); });
} }
@@ -84,7 +84,7 @@ class JWTStorage {
this.timeDelta = new Date().getTime() / 1000 - jwtData.time; this.timeDelta = new Date().getTime() / 1000 - jwtData.time;
this.jwtData.expiration += this.timeDelta - 5 * 60; //Force reduce expiration by 5 minutes this.jwtData.expiration += this.timeDelta - 5 * 60; //Force reduce expiration by 5 minutes
this.jwtData.refresh_expiration += this.timeDelta - 5 * 60; //Force reduce expiration by 5 minutes this.jwtData.refresh_expiration += this.timeDelta - 5 * 60; //Force reduce expiration by 5 minutes
this.logger.info("Update jwt token in local storage")
LocalStorage.setItem('jwt', this.jwtData); LocalStorage.setItem('jwt', this.jwtData);
} }
} }
@@ -147,7 +147,7 @@ class JWTStorage {
} }
async renew(): Promise<JWTDataType> { async renew(): Promise<JWTDataType> {
const token = await ConsoleAPIClient.getNewAccessToken(); const token = await ConsoleAPIClient.renewAccessToken();
if (!token) { if (!token) {
throw new Error('Can not get a new access token'); throw new Error('Can not get a new access token');
@@ -11,8 +11,13 @@ import Application from '../applications/services/application-service';
import { UserType } from '@features/users/types/user'; import { UserType } from '@features/users/types/user';
import { Cookies } from 'react-cookie'; import { Cookies } from 'react-cookie';
import InitService from '../global/services/init-service'; import InitService from '../global/services/init-service';
import { useRecoilState } from "recoil";
import { CurrentUserState } from "features/users/state/atoms/current-user";
class Login extends Observable { class Login extends Observable {
private static logInOngoing = false;
// Promise resolved when user is defined // Promise resolved when user is defined
userIsSet!: Promise<string>; userIsSet!: Promise<string>;
resolveUser!: (userId: string) => void; resolveUser!: (userId: string) => void;
@@ -80,12 +85,14 @@ class Login extends Observable {
} }
if (!AuthService.isInitialized()) { if (!AuthService.isInitialized()) {
this.logger.log("Auth service is not initialized, init ...")
this.reset(); this.reset();
await AuthService.init(); await AuthService.init();
this.logger.info("Auth service initialized");
const redirectUrl = this.cookies.get('pending-redirect'); const redirectUrl = this.cookies.get('pending-redirect');
if (redirectUrl) { if (redirectUrl) {
console.log('Got pending redirect to', redirectUrl); this.logger.info('Got pending redirect to', redirectUrl);
this.cookies.remove('pending-redirect'); this.cookies.remove('pending-redirect');
setTimeout(() => { setTimeout(() => {
document.location.href = redirectUrl; document.location.href = redirectUrl;
@@ -104,6 +111,8 @@ class Login extends Observable {
} }
async updateUser(callback?: (err: Error | null, user?: UserType) => void): Promise<void> { async updateUser(callback?: (err: Error | null, user?: UserType) => void): Promise<void> {
this.logger.info("LoginService:: Try to update user info ")
if (Globals.store_public_access_get_data) { if (Globals.store_public_access_get_data) {
this.firstInit = true; this.firstInit = true;
this.state = 'logged_out'; this.state = 'logged_out';
@@ -114,7 +123,7 @@ class Login extends Observable {
AuthService.updateUser(async user => { AuthService.updateUser(async user => {
this.logger.debug('User update result', user); this.logger.debug('User update result', user);
if (!user) { if (!user) {
if (!this.pingServer()) { if (!(await this.pingServer())) {
//We are disconnected //We are disconnected
console.log('We are disconnected, we will get user again in 10 seconds'); console.log('We are disconnected, we will get user again in 10 seconds');
setTimeout(() => { setTimeout(() => {
@@ -167,15 +176,17 @@ class Login extends Observable {
}); });
} }
login(params: any, hide_load = false) { async login(params: any, hide_load = false) {
if (!hide_load) { if (!Login.logInOngoing) {
this.login_loading = true; this.logger.debug("Try to login");
} if (!hide_load) {
this.login_error = false; this.login_loading = true;
this.notify(); }
this.login_error = false;
this.notify();
AuthService.login(params) try {
.then(async result => { const result = await AuthService.login(params);
this.login_loading = false; this.login_loading = false;
if (!result) { if (!result) {
this.login_error = true; this.login_error = true;
@@ -183,11 +194,15 @@ class Login extends Observable {
return; return;
} }
await this.updateUser(); await this.updateUser();
}) } catch (err) {
.catch(err => {
this.logger.error('Can not login', err); this.logger.error('Can not login', err);
// TODO display a modal message } finally {
}); this.logger.debug('Login process finished');
Login.logInOngoing = false;
}
} else {
this.logger.debug("Login is already in process ...");
}
} }
async logout(reload = false) { async logout(reload = false) {
@@ -8,9 +8,10 @@ import { getAsFrontUrl } from '@features/global/utils/URLUtils';
import { TdriveService } from '../../../global/framework/registry-decorator-service'; import { TdriveService } from '../../../global/framework/registry-decorator-service';
import EnvironmentService from '../../../global/framework/environment-service'; import EnvironmentService from '../../../global/framework/environment-service';
import { AuthProvider, InitParameters } from '../auth-provider'; import { AuthProvider, InitParameters } from '../auth-provider';
import ConsoleService from '@features/console/services/console-service';
import jwtStorageService, { JWTDataType } from '@features/auth/jwt-storage-service'; import jwtStorageService, { JWTDataType } from '@features/auth/jwt-storage-service';
import LocalStorage from '@features/global/framework/local-storage-service'; import LocalStorage from '@features/global/framework/local-storage-service';
import ConsoleApiClient from '@features/console/api/console-api-client';
import JwtStorageService from "@features/auth/jwt-storage-service";
const OIDC_CALLBACK_URL = '/oidccallback'; const OIDC_CALLBACK_URL = '/oidccallback';
const OIDC_SIGNOUT_URL = '/signout'; const OIDC_SIGNOUT_URL = '/signout';
@@ -55,11 +56,11 @@ export default class OIDCAuthProviderService
scope: 'openid profile email address phone offline_access', scope: 'openid profile email address phone offline_access',
post_logout_redirect_uri: getAsFrontUrl(OIDC_SIGNOUT_URL), post_logout_redirect_uri: getAsFrontUrl(OIDC_SIGNOUT_URL),
silent_redirect_uri: getAsFrontUrl(OIDC_SILENT_URL), silent_redirect_uri: getAsFrontUrl(OIDC_SILENT_URL),
automaticSilentRenew: true, automaticSilentRenew: false,
loadUserInfo: true, loadUserInfo: true,
accessTokenExpiringNotificationTime: 10, accessTokenExpiringNotificationTime: 10,
filterProtocolClaims: true, filterProtocolClaims: true,
monitorSession: false, monitorSession: true,
}); });
// For logout if signout or logout endpoint called // For logout if signout or logout endpoint called
@@ -69,6 +70,18 @@ export default class OIDCAuthProviderService
this.signOut(); this.signOut();
} }
this.userManager.events.addUserSessionChanged((... args) => {
this.logger.debug('User Session changed', args);
});
this.userManager.events.addSilentRenewError((... args) => {
this.logger.debug('Silent Renew Error', args);
});
this.userManager.events.addUserUnloaded((... args) => {
this.logger.debug('User unloaded', args);
});
this.userManager.events.addUserLoaded((user: any, ...args) => { this.userManager.events.addUserLoaded((user: any, ...args) => {
this.logger.debug('New User Loaded', user, args); this.logger.debug('New User Loaded', user, args);
this.logger.debug('Acess_token: ', user.access_token); this.logger.debug('Acess_token: ', user.access_token);
@@ -83,12 +96,8 @@ export default class OIDCAuthProviderService
await this.userManager?.removeUser(); await this.userManager?.removeUser();
await this.signIn(); await this.signIn();
}); });
this.userManager.events.addSilentRenewError((...args) => {
console.error('Silent Renew Error', args);
});
} }
this.logger.info("Init completed")
return this; return this;
} }
@@ -96,7 +105,7 @@ export default class OIDCAuthProviderService
this.logger.info('Signin'); this.logger.info('Signin');
try { try {
await this.userManager!.signinRedirectCallback(); await this.userManager?.signinRedirectCallback();
} catch (e) { } catch (e) {
console.log('Not connected, connect through SSO'); console.log('Not connected, connect through SSO');
} }
@@ -104,24 +113,23 @@ export default class OIDCAuthProviderService
const user = await this.userManager?.getUser(); const user = await this.userManager?.getUser();
if (user) { if (user) {
await this.getJWTFromOidcToken(user, (err, jwt) => { try {
if (err) { const jwt = await this.getJWTFromOidcToken(user);
this.logger.error(
'OIDC user loaded listener, error while getting the JWT from OIDC token',
err,
);
this.signinRedirect();
}
if (!this.initialized) { if (!this.initialized) {
this.onInitialized(); this.onInitialized();
this.initialized = true; this.initialized = true;
} else {
jwt && this.params!.onNewToken(jwt);
} }
}); this.logger.info("Setting new access token");
await this.params?.onNewToken(jwt);
} catch (err) {
this.logger.error(
'OIDC user loaded listener, error while getting the JWT from OIDC token',
err,
);
await this.signinRedirect();
}
} else { } else {
this.userManager?.signinRedirect(); await this.signinRedirect();
} }
} }
@@ -154,14 +162,10 @@ export default class OIDCAuthProviderService
* Try to get a new JWT token from the OIDC one: * Try to get a new JWT token from the OIDC one:
* Call the backend with the OIDC token, it will use it to get a new token from console * Call the backend with the OIDC token, it will use it to get a new token from console
*/ */
private async getJWTFromOidcToken( private async getJWTFromOidcToken(user: Oidc.User): Promise<JWTDataType> {
user: Oidc.User,
callback: (err?: Error, accessToken?: JWTDataType) => void,
): Promise<void> {
if (!user) { if (!user) {
this.logger.info('getJWTFromOidcToken, Cannot getJWTFromOidcToken with a null user'); this.logger.info('getJWTFromOidcToken, Cannot getJWTFromOidcToken with a null user');
callback(new Error('Cannot getJWTFromOidcToken with a null user')); throw new Error('Cannot getJWTFromOidcToken with a null user');
return;
} }
if (user.expired) { if (user.expired) {
@@ -169,13 +173,16 @@ export default class OIDCAuthProviderService
this.logger.info('getJWTFromOidcToken, user expired'); this.logger.info('getJWTFromOidcToken, user expired');
} }
ConsoleService.getNewAccessToken( const jwt = await ConsoleApiClient.getNewAccessToken(
{ id_token: user.id_token, access_token: user.access_token }, { id_token: user.id_token, access_token: user.access_token },
callback,
); );
JwtStorageService.updateJWT(jwt)
return jwt;
} }
signinRedirect() { async signinRedirect() {
if (document.location.href.indexOf('/login') === -1) { if (document.location.href.indexOf('/login') === -1) {
//Save requested URL for after redirect / sign-in //Save requested URL for after redirect / sign-in
LocalStorage.setItem('requested_url', { LocalStorage.setItem('requested_url', {
@@ -186,7 +193,9 @@ export default class OIDCAuthProviderService
jwtStorageService.clear(); jwtStorageService.clear();
if (this.userManager) this.userManager.signinRedirect(); if (this.userManager) {
await this.userManager.signinRedirect();
}
} }
onInitialized() { onInitialized() {
@@ -1,6 +1,7 @@
import Api from '@features/global/framework/api-service'; import Api from '@features/global/framework/api-service';
import { TdriveService } from '@features/global/framework/registry-decorator-service'; import { TdriveService } from '@features/global/framework/registry-decorator-service';
import JWTStorage, { JWTDataType } from '@features/auth/jwt-storage-service'; import JWTStorage, { JWTDataType } from '@features/auth/jwt-storage-service';
import Logger from "features/global/framework/logger-service";
type LoginParams = { type LoginParams = {
email: string; email: string;
@@ -16,8 +17,20 @@ type SignupParams = {
username: string; username: string;
}; };
type AccessTokenResponse = {
statusCode: string;
access_token: JWTDataType;
}
type AccessTokenRequest = {
oidc_id_token: string;
}
@TdriveService('ConsoleAPIClientService') @TdriveService('ConsoleAPIClientService')
class ConsoleAPIClient { class ConsoleAPIClient {
logger = Logger.getLogger('ConsoleAPIClient');
login(params: LoginParams, disableJWTAuthentication = false): Promise<string> { login(params: LoginParams, disableJWTAuthentication = false): Promise<string> {
return Api.post<LoginParams, { access_token: string }>( return Api.post<LoginParams, { access_token: string }>(
'/internal/services/console/v1/login', '/internal/services/console/v1/login',
@@ -39,7 +52,29 @@ class ConsoleAPIClient {
return res; return res;
} }
getNewAccessToken(): Promise<JWTDataType> { public async getNewAccessToken(
currentToken: { access_token: string; id_token: string }
): Promise<JWTDataType> {
this.logger.debug(
`getNewAccessToken, get new token from current token ${JSON.stringify(currentToken)}`,
);
const response = await Api.post<AccessTokenRequest, AccessTokenResponse>(
'/internal/services/console/v1/login',
{ oidc_id_token: currentToken.id_token });
if (response.statusCode && !response.access_token) {
this.logger.error(
'getNewAccessToken, Can not retrieve access_token from console. Response was',
response,
);
throw new Error('Can not retrieve access_token from console');
}
// the input access_token is potentially expired and so the response contains an error.
// we should be able to refresh the token or renew it in some way...
return response.access_token;
}
renewAccessToken(): Promise<JWTDataType> {
if (JWTStorage.isRefreshExpired() && JWTStorage.isAccessExpired()) { if (JWTStorage.isRefreshExpired() && JWTStorage.isAccessExpired()) {
throw new Error('Can not get access token as both access and refresh token are expired'); throw new Error('Can not get access token as both access and refresh token are expired');
} }
@@ -103,42 +103,6 @@ class ConsoleService {
return res; return res;
} }
/**
* @deprecated use ConsoleServiceAPIClient.getNewAccessToken
* @param currentToken
* @param callback
*/
public getNewAccessToken(
currentToken: { access_token: string; id_token: string },
callback: (err?: Error, access_token?: JWTDataType) => void,
): void {
this.logger.debug(
`getNewAccessToken, get new token from current token ${JSON.stringify(currentToken)}`,
);
Api.post(
'/internal/services/console/v1/login',
{ oidc_id_token: currentToken.id_token },
(response: {
access_token: JWTDataType;
message: string;
error: string;
statusCode: number;
}) => {
if (response.statusCode && !response.access_token) {
this.logger.error(
'getNewAccessToken, Can not retrieve access_token from console. Response was',
response,
);
callback(new Error('Can not retrieve access_token from console'));
return;
}
// the input access_token is potentially expired and so the response contains an error.
// we should be able to refresh the token or renew it in some way...
callback(undefined, response.access_token);
},
);
}
} }
export default new ConsoleService(); export default new ConsoleService();
@@ -22,7 +22,7 @@ export type RealtimeRoomService<T> = {
* *
* Note: It will subscribe only once, even if the component using it re renders. If you need to unsubscribe and subscribe again, call unsubscribe on the returned object. * Note: It will subscribe only once, even if the component using it re renders. If you need to unsubscribe and subscribe again, call unsubscribe on the returned object.
* *
* @param roomName * @param roomConf
* @param tagName * @param tagName
* @param onEvent * @param onEvent
* @returns * @returns
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from "react";
import LoginService from '@features/auth/login-service'; import LoginService from '@features/auth/login-service';
import UserAPIClient from '@features/users/api/user-api-client'; import UserAPIClient from '@features/users/api/user-api-client';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
@@ -8,19 +8,29 @@ import { useRealtimeRoom } from '@features/global/hooks/use-realtime';
import Languages from '@features/global/services/languages-service'; import Languages from '@features/global/services/languages-service';
import { useSetUserList } from './use-user-list'; import { useSetUserList } from './use-user-list';
import { getPublicLinkToken } from 'app/features/drive/api-client/api-client'; import { getPublicLinkToken } from 'app/features/drive/api-client/api-client';
import Logger from '../../../features/global/framework/logger-service';
export const useCurrentUser = () => { export const useCurrentUser = () => {
const [user, setUser] = useRecoilState(CurrentUserState); const [user, setUser] = useRecoilState(CurrentUserState);
const { set: setUserList } = useSetUserList('useCurrentUser'); const { set: setUserList } = useSetUserList('useCurrentUser');
const logger = Logger.getLogger('useCurrentUser');
//Depreciated way to get use update from LoginService //Depreciated way to get use update from LoginService
LoginService.recoilUpdateUser = setUser; LoginService.recoilUpdateUser = setUser;
useEffect(() => { useEffect(() => {
if (!user && !getPublicLinkToken()) { if (!user && !getPublicLinkToken()) {
LoginService.init(); logger.debug("Init LoggerService ...");
LoginService.login({}); LoginService.init(true)
.then(() => logger.debug("Init LoggerService completed"))
.then(() => LoginService.login({}))
.then(() => logger.debug("Login process completed"))
.then(() => {if (user) setUserList([user])})
.catch(err => logger.error("Error during auth: ", err))
} else {
if (user) setUserList([user]);
} }
if (user) setUserList([user]);
}, [user]); }, [user]);
//Update app language //Update app language
@@ -2,32 +2,35 @@ import { useEffect } from 'react';
import { useRecoilCallback, useRecoilValue } from 'recoil'; import { useRecoilCallback, useRecoilValue } from 'recoil';
import { OnlineUserStateFamily, OnlineUserType } from '../state/atoms/online-users'; import { OnlineUserStateFamily, OnlineUserType } from '../state/atoms/online-users';
import { OnlineUserRealtimeAPI } from '../api/online-user-realtime-api-client'; //TODO refactor it in the notification feature
import WebSocketFactory from '../../global/services/websocket-factory-service';
// import { OnlineUserRealtimeAPI } from '../api/online-user-realtime-api-client';
// import WebSocketFactory from '../../global/services/websocket-factory-service';
export const useOnlineUser = (id: string): OnlineUserType => { export const useOnlineUser = (id: string): OnlineUserType => {
const OnlineAPI = OnlineUserRealtimeAPI(WebSocketFactory.get());
const updateUser = useRecoilCallback( // const OnlineAPI = OnlineUserRealtimeAPI(WebSocketFactory.get());
({ set, snapshot }) =>
(status: { id: string; connected: boolean }) => { // const updateUser = useRecoilCallback(
const current = snapshot.getLoadable(OnlineUserStateFamily(status.id)).contents; // ({ set, snapshot }) =>
set(OnlineUserStateFamily(status.id), { // (status: { id: string; connected: boolean }) => {
...status, // const current = snapshot.getLoadable(OnlineUserStateFamily(status.id)).contents;
lastSeen: status.connected ? Date.now() : current.lastSeen, // set(OnlineUserStateFamily(status.id), {
initialized: true, // ...status,
}); // lastSeen: status.connected ? Date.now() : current.lastSeen,
}, // initialized: true,
[], // });
); // },
// [],
// );
const state = useRecoilValue(OnlineUserStateFamily(id)); const state = useRecoilValue(OnlineUserStateFamily(id));
useEffect(() => { useEffect(() => {
if (state && !state.initialized) { if (state && !state.initialized) {
OnlineAPI.getUserStatus(id).then(status => { // OnlineAPI.getUserStatus(id).then(status => {
updateUser({ id: status[0], connected: status[1] }); // updateUser({ id: status[0], connected: status[1] });
}); // });
} }
}, [state, id]); }, [state, id]);