🐛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
closable={false}
title={null}
visible={open}
open={open}
footer={null}
destroyOnClose={true}
width={ModalManager.getPosition()?.size?.width || '700px'}
@@ -50,6 +50,7 @@ class AuthService {
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 (accountType === 'remote') {
@@ -104,7 +105,6 @@ class AuthService {
onSessionExpired: () => this.onSessionExpired(),
onNewToken: async token => {
this.onNewToken(token);
// TODO: Change the basic auth to return this new token on init
if (this.initState === 'initializing') {
const user = await this.comleteInit();
@@ -118,6 +118,8 @@ class AuthService {
resolve(null);
},
});
this.logger.info("Init completed")
resolve(null);
});
}
@@ -84,7 +84,7 @@ class JWTStorage {
this.timeDelta = new Date().getTime() / 1000 - jwtData.time;
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.logger.info("Update jwt token in local storage")
LocalStorage.setItem('jwt', this.jwtData);
}
}
@@ -147,7 +147,7 @@ class JWTStorage {
}
async renew(): Promise<JWTDataType> {
const token = await ConsoleAPIClient.getNewAccessToken();
const token = await ConsoleAPIClient.renewAccessToken();
if (!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 { Cookies } from 'react-cookie';
import InitService from '../global/services/init-service';
import { useRecoilState } from "recoil";
import { CurrentUserState } from "features/users/state/atoms/current-user";
class Login extends Observable {
private static logInOngoing = false;
// Promise resolved when user is defined
userIsSet!: Promise<string>;
resolveUser!: (userId: string) => void;
@@ -80,12 +85,14 @@ class Login extends Observable {
}
if (!AuthService.isInitialized()) {
this.logger.log("Auth service is not initialized, init ...")
this.reset();
await AuthService.init();
this.logger.info("Auth service initialized");
const redirectUrl = this.cookies.get('pending-redirect');
if (redirectUrl) {
console.log('Got pending redirect to', redirectUrl);
this.logger.info('Got pending redirect to', redirectUrl);
this.cookies.remove('pending-redirect');
setTimeout(() => {
document.location.href = redirectUrl;
@@ -104,6 +111,8 @@ class Login extends Observable {
}
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) {
this.firstInit = true;
this.state = 'logged_out';
@@ -114,7 +123,7 @@ class Login extends Observable {
AuthService.updateUser(async user => {
this.logger.debug('User update result', user);
if (!user) {
if (!this.pingServer()) {
if (!(await this.pingServer())) {
//We are disconnected
console.log('We are disconnected, we will get user again in 10 seconds');
setTimeout(() => {
@@ -167,15 +176,17 @@ class Login extends Observable {
});
}
login(params: any, hide_load = false) {
if (!hide_load) {
this.login_loading = true;
}
this.login_error = false;
this.notify();
async login(params: any, hide_load = false) {
if (!Login.logInOngoing) {
this.logger.debug("Try to login");
if (!hide_load) {
this.login_loading = true;
}
this.login_error = false;
this.notify();
AuthService.login(params)
.then(async result => {
try {
const result = await AuthService.login(params);
this.login_loading = false;
if (!result) {
this.login_error = true;
@@ -183,11 +194,15 @@ class Login extends Observable {
return;
}
await this.updateUser();
})
.catch(err => {
} catch (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) {
@@ -8,9 +8,10 @@ import { getAsFrontUrl } from '@features/global/utils/URLUtils';
import { TdriveService } from '../../../global/framework/registry-decorator-service';
import EnvironmentService from '../../../global/framework/environment-service';
import { AuthProvider, InitParameters } from '../auth-provider';
import ConsoleService from '@features/console/services/console-service';
import jwtStorageService, { JWTDataType } from '@features/auth/jwt-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_SIGNOUT_URL = '/signout';
@@ -55,11 +56,11 @@ export default class OIDCAuthProviderService
scope: 'openid profile email address phone offline_access',
post_logout_redirect_uri: getAsFrontUrl(OIDC_SIGNOUT_URL),
silent_redirect_uri: getAsFrontUrl(OIDC_SILENT_URL),
automaticSilentRenew: true,
automaticSilentRenew: false,
loadUserInfo: true,
accessTokenExpiringNotificationTime: 10,
filterProtocolClaims: true,
monitorSession: false,
monitorSession: true,
});
// For logout if signout or logout endpoint called
@@ -69,6 +70,18 @@ export default class OIDCAuthProviderService
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.logger.debug('New User Loaded', user, args);
this.logger.debug('Acess_token: ', user.access_token);
@@ -83,12 +96,8 @@ export default class OIDCAuthProviderService
await this.userManager?.removeUser();
await this.signIn();
});
this.userManager.events.addSilentRenewError((...args) => {
console.error('Silent Renew Error', args);
});
}
this.logger.info("Init completed")
return this;
}
@@ -96,7 +105,7 @@ export default class OIDCAuthProviderService
this.logger.info('Signin');
try {
await this.userManager!.signinRedirectCallback();
await this.userManager?.signinRedirectCallback();
} catch (e) {
console.log('Not connected, connect through SSO');
}
@@ -104,24 +113,23 @@ export default class OIDCAuthProviderService
const user = await this.userManager?.getUser();
if (user) {
await this.getJWTFromOidcToken(user, (err, jwt) => {
if (err) {
this.logger.error(
'OIDC user loaded listener, error while getting the JWT from OIDC token',
err,
);
this.signinRedirect();
}
try {
const jwt = await this.getJWTFromOidcToken(user);
if (!this.initialized) {
this.onInitialized();
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 {
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:
* Call the backend with the OIDC token, it will use it to get a new token from console
*/
private async getJWTFromOidcToken(
user: Oidc.User,
callback: (err?: Error, accessToken?: JWTDataType) => void,
): Promise<void> {
private async getJWTFromOidcToken(user: Oidc.User): Promise<JWTDataType> {
if (!user) {
this.logger.info('getJWTFromOidcToken, Cannot getJWTFromOidcToken with a null user');
callback(new Error('Cannot getJWTFromOidcToken with a null user'));
return;
throw new Error('Cannot getJWTFromOidcToken with a null user');
}
if (user.expired) {
@@ -169,13 +173,16 @@ export default class OIDCAuthProviderService
this.logger.info('getJWTFromOidcToken, user expired');
}
ConsoleService.getNewAccessToken(
const jwt = await ConsoleApiClient.getNewAccessToken(
{ 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) {
//Save requested URL for after redirect / sign-in
LocalStorage.setItem('requested_url', {
@@ -186,7 +193,9 @@ export default class OIDCAuthProviderService
jwtStorageService.clear();
if (this.userManager) this.userManager.signinRedirect();
if (this.userManager) {
await this.userManager.signinRedirect();
}
}
onInitialized() {
@@ -1,6 +1,7 @@
import Api from '@features/global/framework/api-service';
import { TdriveService } from '@features/global/framework/registry-decorator-service';
import JWTStorage, { JWTDataType } from '@features/auth/jwt-storage-service';
import Logger from "features/global/framework/logger-service";
type LoginParams = {
email: string;
@@ -16,8 +17,20 @@ type SignupParams = {
username: string;
};
type AccessTokenResponse = {
statusCode: string;
access_token: JWTDataType;
}
type AccessTokenRequest = {
oidc_id_token: string;
}
@TdriveService('ConsoleAPIClientService')
class ConsoleAPIClient {
logger = Logger.getLogger('ConsoleAPIClient');
login(params: LoginParams, disableJWTAuthentication = false): Promise<string> {
return Api.post<LoginParams, { access_token: string }>(
'/internal/services/console/v1/login',
@@ -39,7 +52,29 @@ class ConsoleAPIClient {
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()) {
throw new Error('Can not get access token as both access and refresh token are expired');
}
@@ -103,42 +103,6 @@ class ConsoleService {
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();
@@ -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.
*
* @param roomName
* @param roomConf
* @param tagName
* @param onEvent
* @returns
@@ -1,5 +1,5 @@
/* 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 UserAPIClient from '@features/users/api/user-api-client';
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 { useSetUserList } from './use-user-list';
import { getPublicLinkToken } from 'app/features/drive/api-client/api-client';
import Logger from '../../../features/global/framework/logger-service';
export const useCurrentUser = () => {
const [user, setUser] = useRecoilState(CurrentUserState);
const { set: setUserList } = useSetUserList('useCurrentUser');
const logger = Logger.getLogger('useCurrentUser');
//Depreciated way to get use update from LoginService
LoginService.recoilUpdateUser = setUser;
useEffect(() => {
if (!user && !getPublicLinkToken()) {
LoginService.init();
LoginService.login({});
logger.debug("Init LoggerService ...");
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]);
//Update app language
@@ -2,32 +2,35 @@ import { useEffect } from 'react';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { OnlineUserStateFamily, OnlineUserType } from '../state/atoms/online-users';
import { OnlineUserRealtimeAPI } from '../api/online-user-realtime-api-client';
import WebSocketFactory from '../../global/services/websocket-factory-service';
//TODO refactor it in the notification feature
// import { OnlineUserRealtimeAPI } from '../api/online-user-realtime-api-client';
// import WebSocketFactory from '../../global/services/websocket-factory-service';
export const useOnlineUser = (id: string): OnlineUserType => {
const OnlineAPI = OnlineUserRealtimeAPI(WebSocketFactory.get());
const updateUser = useRecoilCallback(
({ set, snapshot }) =>
(status: { id: string; connected: boolean }) => {
const current = snapshot.getLoadable(OnlineUserStateFamily(status.id)).contents;
set(OnlineUserStateFamily(status.id), {
...status,
lastSeen: status.connected ? Date.now() : current.lastSeen,
initialized: true,
});
},
[],
);
// const OnlineAPI = OnlineUserRealtimeAPI(WebSocketFactory.get());
// const updateUser = useRecoilCallback(
// ({ set, snapshot }) =>
// (status: { id: string; connected: boolean }) => {
// const current = snapshot.getLoadable(OnlineUserStateFamily(status.id)).contents;
// set(OnlineUserStateFamily(status.id), {
// ...status,
// lastSeen: status.connected ? Date.now() : current.lastSeen,
// initialized: true,
// });
// },
// [],
// );
const state = useRecoilValue(OnlineUserStateFamily(id));
useEffect(() => {
if (state && !state.initialized) {
OnlineAPI.getUserStatus(id).then(status => {
updateUser({ id: status[0], connected: status[1] });
});
// OnlineAPI.getUserStatus(id).then(status => {
// updateUser({ id: status[0], connected: status[1] });
// });
}
}, [state, id]);