From ce8439adfe32aa492e75d43e0bad76530edc4434 Mon Sep 17 00:00:00 2001 From: Evans Onoka Date: Fri, 27 Aug 2021 11:14:59 +0300 Subject: [PATCH] Enhancement: error hints (#1060) * initial commit on auth error hints * added more errors to error library * fixed auth errors and added error tips * updating local branch with dev * removed interaction_status from cookies * added return type to createCookie * removed uri extension and added consent error tips * error hints now appear below error-code * fixed formating errors * maintained fluentui at 8.28.0 * adjustment to hints wording * corrected hinting language * code refactors in error hinting --- .vscode/settings.json | 37 +++--- package-lock.json | 87 +++++++----- package.json | 2 +- .../actions/permissions-action-creator.ts | 4 +- src/app/views/App.tsx | 78 +++-------- src/app/views/app-sections/StatusMessages.tsx | 4 +- .../views/authentication/Authentication.tsx | 124 ++++++++++++------ .../AuthenticationErrorsHints.ts | 32 +++++ src/index.tsx | 20 +-- src/messages/GE.json | 29 +++- .../authentication/AuthenticationWrapper.ts | 111 ++++++++++++---- src/modules/authentication/authUtils.ts | 2 +- src/types/status.ts | 1 + 13 files changed, 331 insertions(+), 200 deletions(-) create mode 100644 src/app/views/authentication/AuthenticationErrorsHints.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index cd8db0543..58979303d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,21 +1,18 @@ { - "search.exclude": { - "**/node_modules": true, - "**/all.min.js": true - }, - "files.exclude": { - "**/node_modules": true, - "**/.github": true, - }, - "files.trimTrailingWhitespace": true, - "editor.codeActionsOnSave": { - "source.fixAll.tslint": true - }, - "editor.formatOnSave": true, - "editor.formatOnPaste": true, - "typescript.updateImportsOnFileMove.enabled": "always", - "eslint.validate": [ - "typescript", - "typescriptreact" - ], -} \ No newline at end of file + "search.exclude": { + "**/node_modules": true, + "**/all.min.js": true + }, + "files.exclude": { + "**/node_modules": true, + "**/.github": true + }, + "files.trimTrailingWhitespace": true, + "editor.codeActionsOnSave": { + "source.fixAll.tslint": true + }, + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "typescript.updateImportsOnFileMove.enabled": "always", + "eslint.validate": ["typescript", "typescriptreact"] +} diff --git a/package-lock.json b/package-lock.json index 50fd0c951..2fdcd2916 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1685,6 +1685,22 @@ "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==" }, + "@fluentui/date-time-utilities": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@fluentui/date-time-utilities/-/date-time-utilities-8.2.2.tgz", + "integrity": "sha512-djHrX/38ty+F93qLQjzmRzPzK598CW9g/RPhQH6GyrFBLPSWM1swYKB5TP6E7FrIf+fT4pVqrNUSYZhgi2rrOQ==", + "requires": { + "@fluentui/set-version": "^8.1.4", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + } + } + }, "@fluentui/dom-utilities": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@fluentui/dom-utilities/-/dom-utilities-2.1.4.tgz", @@ -1737,6 +1753,21 @@ } } }, + "@fluentui/keyboard-key": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.3.4.tgz", + "integrity": "sha512-pVY2m3IC5+LLmMzsaPApX9eKTzpOzdgQwrR3FNTE6mGx3N/+QWYM7fdF+T1ldZQt87dCRSeQnmAo5kqjtxeA/w==", + "requires": { + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + } + } + }, "@fluentui/merge-styles": { "version": "8.1.4", "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.1.4.tgz", @@ -1773,36 +1804,26 @@ "tslib": "^2.1.0" }, "dependencies": { - "@fluentui/date-time-utilities": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@fluentui/date-time-utilities/-/date-time-utilities-8.2.2.tgz", - "integrity": "sha512-djHrX/38ty+F93qLQjzmRzPzK598CW9g/RPhQH6GyrFBLPSWM1swYKB5TP6E7FrIf+fT4pVqrNUSYZhgi2rrOQ==", - "requires": { - "@fluentui/set-version": "^8.1.4", - "tslib": "^2.1.0" - } - }, - "@fluentui/keyboard-key": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.3.4.tgz", - "integrity": "sha512-pVY2m3IC5+LLmMzsaPApX9eKTzpOzdgQwrR3FNTE6mGx3N/+QWYM7fdF+T1ldZQt87dCRSeQnmAo5kqjtxeA/w==", - "requires": { - "tslib": "^2.1.0" - } - }, - "@fluentui/react-focus": { - "version": "8.1.10", - "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.1.10.tgz", - "integrity": "sha512-sojXA6epu2QJbFf+XqP1AHOrrWssoQJWJNuzp0MCzQOWCUlLLqRpRUHtUKZzCnrbD9G5MOW8/192m/rSPyM7eA==", - "requires": { - "@fluentui/keyboard-key": "^0.3.4", - "@fluentui/merge-styles": "^8.1.4", - "@fluentui/set-version": "^8.1.4", - "@fluentui/style-utilities": "^8.2.2", - "@fluentui/utilities": "^8.2.2", - "tslib": "^2.1.0" - } - }, + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + } + } + }, + "@fluentui/react-focus": { + "version": "8.1.10", + "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.1.10.tgz", + "integrity": "sha512-sojXA6epu2QJbFf+XqP1AHOrrWssoQJWJNuzp0MCzQOWCUlLLqRpRUHtUKZzCnrbD9G5MOW8/192m/rSPyM7eA==", + "requires": { + "@fluentui/keyboard-key": "^0.3.4", + "@fluentui/merge-styles": "^8.1.4", + "@fluentui/set-version": "^8.1.4", + "@fluentui/style-utilities": "^8.2.2", + "@fluentui/utilities": "^8.2.2", + "tslib": "^2.1.0" + }, + "dependencies": { "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", @@ -2829,9 +2850,9 @@ } }, "@microsoft/load-themed-styles": { - "version": "1.10.58", - "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.58.tgz", - "integrity": "sha512-/aRXzHhaloOUdEPq1bUNYkriDGz984BUQcOab8222NWpTwycUad/dqjTlG8iI51vXqj4RFr1XGqUfamXBOIM+w==" + "version": "1.10.202", + "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.202.tgz", + "integrity": "sha512-pWoN9hl1vfXnPfu2tS5VndXXKMe+UEWLJXDKNGXSNpmfszVLzG8Ns0TlZHlwtgpSaSD3f0kdVDfqAek8aflD4w==" }, "@microsoft/microsoft-graph-client": { "version": "2.1.0", diff --git a/package.json b/package.json index 41542356b..fe912316b 100644 --- a/package.json +++ b/package.json @@ -154,4 +154,4 @@ "ws": "8.0.0", "yargs-parser": "13.1.2" } -} \ No newline at end of file +} diff --git a/src/app/services/actions/permissions-action-creator.ts b/src/app/services/actions/permissions-action-creator.ts index 459ad39b0..e350dbde4 100644 --- a/src/app/services/actions/permissions-action-creator.ts +++ b/src/app/services/actions/permissions-action-creator.ts @@ -9,6 +9,7 @@ import { IRootState } from '../../../types/root'; import { sanitizeQueryUrl } from '../../utils/query-url-sanitization'; import { parseSampleUrl } from '../../utils/sample-url-generation'; import { translateMessage } from '../../utils/translate-messages'; +import { getConsentAuthErrorHint } from '../../views/authentication/AuthenticationErrorsHints'; import { ACCOUNT_TYPE, PERMS_SCOPE } from '../graph-constants'; import { FETCH_SCOPES_ERROR, @@ -118,9 +119,10 @@ export function consentToScopes(scopes: string[]): Function { dispatch( setQueryResponseStatus({ statusText: translateMessage('Scope consent failed'), - status: errorCode, + status:errorCode, ok: false, messageType: MessageBarType.error, + hint: getConsentAuthErrorHint(errorCode) }) ); } diff --git a/src/app/views/App.tsx b/src/app/views/App.tsx index f963cc746..68b6c7d8b 100644 --- a/src/app/views/App.tsx +++ b/src/app/views/App.tsx @@ -1,9 +1,4 @@ -import { - Announced, - IStackTokens, - ITheme, - styled, -} from '@fluentui/react'; +import { Announced, IStackTokens, ITheme, styled } from '@fluentui/react'; import React, { Component } from 'react'; import { InjectedIntl, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; @@ -15,11 +10,7 @@ import { componentNames, eventTypes, telemetry } from '../../telemetry'; import { loadGETheme } from '../../themes'; import { ThemeContext } from '../../themes/theme-context'; import { Mode } from '../../types/enums'; -import { - IInitMessage, - IQuery, - IThemeChangedMessage, -} from '../../types/query-runner'; +import { IInitMessage, IQuery, IThemeChangedMessage } from '../../types/query-runner'; import { IRootState } from '../../types/root'; import { ISharedQueryParams } from '../../types/share-query'; import { ISidebarProps } from '../../types/sidebar'; @@ -34,10 +25,7 @@ import { GRAPH_URL } from '../services/graph-constants'; import { parseSampleUrl } from '../utils/sample-url-generation'; import { substituteTokens } from '../utils/token-helpers'; import { translateMessage } from '../utils/translate-messages'; -import { - appTitleDisplayOnFullScreen, - appTitleDisplayOnMobileScreen, -} from './app-sections/AppTitle'; +import { appTitleDisplayOnFullScreen, appTitleDisplayOnMobileScreen } from './app-sections/AppTitle'; import { headerMessaging } from './app-sections/HeaderMessaging'; import { statusMessages } from './app-sections/StatusMessages'; import { termsOfUseMessage } from './app-sections/TermsOfUseMessage'; @@ -157,8 +145,7 @@ class App extends Component { } private generateQueryObjectFrom(queryParams: any) { - const { request, method, version, graphUrl, requestBody, headers } = - queryParams; + const { request, method, version, graphUrl, requestBody, headers } = queryParams; if (!request) { return null; @@ -273,9 +260,11 @@ class App extends Component { const properties = { ...sidebarProperties }; properties.showSidebar = !properties.showSidebar; this.props.actions!.toggleSidebar(properties); - telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, { - ComponentName: componentNames.SIDEBAR_HAMBURGER_BUTTON, - }); + telemetry.trackEvent( + eventTypes.BUTTON_CLICK_EVENT, + { + ComponentName: componentNames.SIDEBAR_HAMBURGER_BUTTON, + }); }; public displayToggleButton = (mediaQueryList: any) => { @@ -301,8 +290,8 @@ class App extends Component { justifyContent: minimised ? '' : 'center', alignItems: minimised ? '' : 'center', marginLeft: minimised ? '' : '-0.9em', - }} - > + + }}>
@@ -315,17 +304,9 @@ class App extends Component { public render() { const classes = classNames(this.props); - const { - authenticated, - graphExplorerMode, - queryState, - minimised, - termsOfUse, - sampleQuery, - actions, - sidebarProperties, - intl: { messages }, - }: any = this.props; + const { authenticated, graphExplorerMode, queryState, minimised, termsOfUse, sampleQuery, + actions, sidebarProperties, intl: { messages }, }: any = this.props; + const query = createShareLink(sampleQuery, authenticated); const sampleHeaderText = messages['Sample Queries']; // tslint:disable-next-line:no-string-literal @@ -364,25 +345,18 @@ class App extends Component { // @ts-ignore
- +
{graphExplorerMode === Mode.Complete && (
- {mobileScreen && - appTitleDisplayOnMobileScreen( + {mobileScreen && appTitleDisplayOnMobileScreen( stackTokens, classes, this.toggleSidebar )} - {!mobileScreen && - appTitleDisplayOnFullScreen( + {!mobileScreen && appTitleDisplayOnFullScreen( classes, minimised, this.toggleSidebar @@ -395,10 +369,7 @@ class App extends Component { {showSidebar && ( <> - + )}
@@ -428,15 +399,8 @@ class App extends Component { } } -const mapStateToProps = ({ - sidebarProperties, - theme, - queryRunnerStatus, - profile, - sampleQuery, - termsOfUse, - authToken, - graphExplorerMode, +const mapStateToProps = ({ sidebarProperties, theme, + queryRunnerStatus, profile, sampleQuery, termsOfUse, authToken, graphExplorerMode, }: IRootState) => { const mobileScreen = !!sidebarProperties.mobileScreen; const showSidebar = !!sidebarProperties.showSidebar; diff --git a/src/app/views/app-sections/StatusMessages.tsx b/src/app/views/app-sections/StatusMessages.tsx index f5561a174..fa32a087f 100644 --- a/src/app/views/app-sections/StatusMessages.tsx +++ b/src/app/views/app-sections/StatusMessages.tsx @@ -43,7 +43,7 @@ export function statusMessages(queryState: any, sampleQuery: IQuery, actions: an }; if (queryState) { - const { messageType, statusText, status, duration } = queryState; + const { messageType, statusText, status, duration, hint } = queryState; let urls: any = {}; let message = status; const extractedUrls = extractUrl(status); @@ -72,6 +72,8 @@ export function statusMessages(queryState: any, sampleQuery: IQuery, actions: an } + {hint &&
{hint}
} + ); } } diff --git a/src/app/views/authentication/Authentication.tsx b/src/app/views/authentication/Authentication.tsx index cebb53d8b..978fa8a24 100644 --- a/src/app/views/authentication/Authentication.tsx +++ b/src/app/views/authentication/Authentication.tsx @@ -1,25 +1,25 @@ - import { SeverityLevel } from '@microsoft/applicationinsights-web'; import { Icon, Label, MessageBar, MessageBarType, Spinner, SpinnerSize, styled } from '@fluentui/react'; import React, { useState } from 'react'; import { FormattedMessage, injectIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { authenticationWrapper } from '../../../modules/authentication'; - import { componentNames, errorTypes, telemetry } from '../../../telemetry'; import { Mode } from '../../../types/enums'; import { IRootState } from '../../../types/root'; import { getAuthTokenSuccess, getConsentedScopesSuccess } from '../../services/actions/auth-action-creators'; import { setQueryResponseStatus } from '../../services/actions/query-status-action-creator'; -import { translateMessage } from '../../utils/translate-messages'; import { classNames } from '../classnames'; import { showSignInButtonOrProfile } from './auth-util-components'; import { authenticationStyles } from './Authentication.styles'; +import { getSignInAuthErrorHint, signInAuthError } from './AuthenticationErrorsHints'; const Authentication = (props: any) => { const dispatch = useDispatch(); const [loginInProgress, setLoginInProgress] = useState(false); - const { sidebarProperties, authToken, graphExplorerMode } = useSelector((state: IRootState) => state); + const { sidebarProperties, authToken, graphExplorerMode } = useSelector( + (state: IRootState) => state + ); const mobileScreen = !!sidebarProperties.mobileScreen; const showSidebar = !!sidebarProperties.showSidebar; const tokenPresent = !!authToken.token; @@ -38,52 +38,77 @@ const Authentication = (props: any) => { const authResponse = await authenticationWrapper.logIn(); if (authResponse) { setLoginInProgress(false); - dispatch(getAuthTokenSuccess(!!authResponse.accessToken)) - dispatch(getConsentedScopesSuccess(authResponse.scopes)) + dispatch(getAuthTokenSuccess(!!authResponse.accessToken)); + dispatch(getConsentedScopesSuccess(authResponse.scopes)); } } catch (error) { const { errorCode } = error; - dispatch(setQueryResponseStatus({ - ok: false, - statusText: messages['Authentication failed'], - status: errorCode === 'popup_window_error' - ? translateMessage('popup blocked, allow pop-up windows in your browser') - : errorCode ? errorCode.replace('_', ' ') : '', - messageType: MessageBarType.error - })); + if(signInAuthError(errorCode)) { + authenticationWrapper.clearSession(); + } + dispatch( + setQueryResponseStatus({ + ok: false, + statusText: messages['Authentication failed'], + status: removeUnderScore(errorCode), + messageType: MessageBarType.error, + hint: getSignInAuthErrorHint(errorCode) + }) + ); setLoginInProgress(false); telemetry.trackException( new Error(errorTypes.OPERATIONAL_ERROR), SeverityLevel.Error, { ComponentName: componentNames.AUTHENTICATION_ACTION, - Message: `Authentication failed: ${errorCode ? errorCode.replace('_', ' ') : ''}`, - }); + Message: `Authentication failed: ${ + errorCode ? removeUnderScore(errorCode) : '' + }`, + } + ); } }; - const showProgressSpinner = (): React.ReactNode => { - return
- - {!minimised && } -
; + const removeUnderScore = (statusString: string): string => { + if(statusString === '' ){ + return statusString; + } + else{ + return statusString.replace(/_/g, ' '); + } } + const showProgressSpinner = (): React.ReactNode => { + return ( +
+ + {!minimised && ( + + )} +
+ ); + }; + const showUnAuthenticatedText = (): React.ReactNode => { - return <> - + return ( + <> + -
- - - - ; - } +
+ + {' '} + + + + ); + }; if (logoutInProgress) { return showProgressSpinner(); @@ -91,16 +116,29 @@ const Authentication = (props: any) => { return ( <> - {loginInProgress ? showProgressSpinner() - : - mobileScreen ? showSignInButtonOrProfile(tokenPresent, mobileScreen, signIn, minimised) : - <> - {!tokenPresent && graphExplorerMode === Mode.Complete && !minimised && showUnAuthenticatedText()} -
{showSignInButtonOrProfile(tokenPresent, mobileScreen, signIn, minimised)}
- } + {loginInProgress ? ( + showProgressSpinner() + ) : mobileScreen ? ( + showSignInButtonOrProfile(tokenPresent, mobileScreen, signIn, minimised) + ) : ( + <> + {!tokenPresent && + graphExplorerMode === Mode.Complete && + !minimised && + showUnAuthenticatedText()} +
+ {showSignInButtonOrProfile( + tokenPresent, + mobileScreen, + signIn, + minimised + )} +
+ + )} - ) -} + ); +}; // @ts-ignore const IntlAuthentication = injectIntl(Authentication); diff --git a/src/app/views/authentication/AuthenticationErrorsHints.ts b/src/app/views/authentication/AuthenticationErrorsHints.ts new file mode 100644 index 000000000..2de5af827 --- /dev/null +++ b/src/app/views/authentication/AuthenticationErrorsHints.ts @@ -0,0 +1,32 @@ +import { translateMessage } from "../../utils/translate-messages"; + +const authErrorList : string[] = ['user_cancelled','null_or_empty_id_token', +"authorization_code_missing_from_server_response", +'no_tokens_found', 'invalid_request', "user_login_error","nonce_mismatch_error", +'login_progress_error', 'interaction_in_progress', +'interaction_required', 'invalid_grant', 'endpoints_resolution_error'] + +const scopeErrorList : string[] = ['interaction_required', 'consent_required', 'login_required', 'access_denied', 'user_cancelled' ] + +export function getSignInAuthErrorHint(error: string): string{ + const authErrorHintAvailable = signInAuthError(error); + return authErrorHintAvailable ? getHint(error) : ''; +} + +export function signInAuthError(error: string): boolean { + return authErrorList.includes(error.trim()); +} + +export function getConsentAuthErrorHint(error: string): string{ + const authErrorHintAvailable = scopeAuthError(error); + const consentError = error + '_consent'; + return authErrorHintAvailable ? getHint(consentError) : ''; +} + +export function scopeAuthError(error: string): boolean { + return scopeErrorList.includes(error); +} + +function getHint( error: string): string { + return translateMessage('Tip') + ' - ' + translateMessage(error); +} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 88a3aeca3..a9057996b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -14,10 +14,7 @@ import pt from 'react-intl/locale-data/pt'; import ru from 'react-intl/locale-data/ru'; import zh from 'react-intl/locale-data/zh'; import { Provider } from 'react-redux'; -import { - getAuthTokenSuccess, - getConsentedScopesSuccess, -} from './app/services/actions/auth-action-creators'; +import { getAuthTokenSuccess, getConsentedScopesSuccess } from './app/services/actions/auth-action-creators'; import { setDevxApiUrl } from './app/services/actions/devxApi-action-creators'; import { setGraphExplorerMode } from './app/services/actions/explorer-mode-action-creator'; import { getGraphProxyUrl } from './app/services/actions/proxy-action-creator'; @@ -115,9 +112,7 @@ setCurrentSystemTheme(); appState.dispatch(getGraphProxyUrl()); function refreshAccessToken() { - authenticationWrapper - .getToken() - .then((authResponse: AuthenticationResult) => { + authenticationWrapper.getToken().then((authResponse: AuthenticationResult) => { if (authResponse && authResponse.accessToken) { appState.dispatch(getAuthTokenSuccess(true)); appState.dispatch(getConsentedScopesSuccess(authResponse.scopes)); @@ -188,11 +183,11 @@ enum Workers { function getWorkerFor(worker: string): string { // tslint:disable-next-line:max-line-length - const WORKER_PATH = - 'https://graphstagingblobstorage.blob.core.windows.net/staging/vendor/bower_components/explorer-v2/build'; + const WORKER_PATH = 'https://graphstagingblobstorage.blob.core.windows.net/staging/vendor/bower_components/explorer-v2/build'; return `data:text/javascript;charset=utf-8,${encodeURIComponent(` - importScripts('${WORKER_PATH}/${worker}.worker.js');`)}`; + importScripts('${WORKER_PATH}/${worker}.worker.js');` + )}`; } const telemetryProvider: ITelemetry = telemetry; @@ -201,10 +196,7 @@ telemetryProvider.initialize(); const Root = () => { return ( - + diff --git a/src/messages/GE.json b/src/messages/GE.json index 68e6f54cd..15b497374 100644 --- a/src/messages/GE.json +++ b/src/messages/GE.json @@ -239,7 +239,7 @@ "Canary": "Canary", "disable canary": "disable canary", "using canary": "You are using the canary version of Graph Explorer. Sign in to make requests", - "use the Microsoft Graph API": "When you use Microsoft Graph APIs, you agree to the", + "use the Microsoft Graph API": "When you use Microsoft Graph APIs, you agree to the ", "Terms of use": "Microsoft APIs Terms of Use", "Microsoft Privacy Statement": "Microsoft Privacy Statement", "Add": "Add", @@ -355,5 +355,28 @@ "Signing you out...": "Signing you out...", "Admin consent not required": "Admin consent is not required", "Navigation help": "Navigate with right arrow key to access", - "Actions menu": " Actions menu" -} \ No newline at end of file + "Actions menu": " Actions menu", + "user_cancelled": "Please wait for the authentication process to finish.", + "popup_window_error": "Please verify that pop-ups are enabled and try again.", + "invalid_client": "Please ask your app administrator to check the app credentials (application client ID) on Azure app registrations.", + "null_or_empty_id_token": "Please sign in again.", + "authorization_code_missing_from_server_response": "Please sign in again.", + "temporarily unavailable": "The server is temporarily unavailable. Please try again later.", + "server-error": "The server is temporarily unavailable. Please try again later.", + "no_tokens_found": "Please sign in again.", + "invalid_request": "Please sign in again.", + "user_login_error": "Please sign in again.", + "nonce_mismatch_error": "Please sign in again.", + "login_in_progress": "Please sign in again.", + "interaction_in_progress": "Please sign in again.", + "interaction_required": "Please sign in again.", + "invalid_grant": "Please sign in again.", + "consent_required": "Please consent to the permissions requested or sign in with a different account.", + "access_denied": "Your permission to access Graph Explorer has been blocked by your tenant admin. Ask your admin to grant you access to these permissions.", + "Tip": "Tip", + "monitor_window_timeout": "Please check your network connection and verify that pop-ups are enabled, and then try again.", + "consent_required_consent": "Please consent to the permission to access this feature.", + "interaction_required_consent": "Please wait for the consent process to finish and verify that pop-ups are enabled.", + "user_cancelled_consent": "Please wait for the consent process to finish.", + "access_denied_consent": "Your consent to this permission has been blocked by your tenant admin. Ask your admin to grant you access and then try again." +} diff --git a/src/modules/authentication/AuthenticationWrapper.ts b/src/modules/authentication/AuthenticationWrapper.ts index 554f65088..8d272c0b8 100644 --- a/src/modules/authentication/AuthenticationWrapper.ts +++ b/src/modules/authentication/AuthenticationWrapper.ts @@ -1,10 +1,17 @@ import { AccountInfo, - AuthenticationResult, InteractionRequiredAuthError, - PopupRequest, SilentRequest + AuthenticationResult, + InteractionRequiredAuthError, + PopupRequest, + SilentRequest, } from '@azure/msal-browser'; -import { AUTH_URL, DEFAULT_USER_SCOPES, HOME_ACCOUNT_KEY } from '../../app/services/graph-constants'; +import { + AUTH_URL, + DEFAULT_USER_SCOPES, + HOME_ACCOUNT_KEY, +} from '../../app/services/graph-constants'; +import { signInAuthError } from '../../app/views/authentication/AuthenticationErrorsHints'; import { geLocale } from '../../appLocale'; import { getCurrentUri } from './authUtils'; import IAuthenticationWrapper from './IAuthenticationWrapper'; @@ -13,13 +20,12 @@ import { msalApplication } from './msal-app'; const defaultScopes = DEFAULT_USER_SCOPES.split(' '); export class AuthenticationWrapper implements IAuthenticationWrapper { - private static instance: AuthenticationWrapper; private consentingToNewScopes: boolean = false; public static getInstance(): AuthenticationWrapper { if (!AuthenticationWrapper.instance) { - AuthenticationWrapper.instance = new AuthenticationWrapper() + AuthenticationWrapper.instance = new AuthenticationWrapper(); } return AuthenticationWrapper.instance; } @@ -48,21 +54,23 @@ export class AuthenticationWrapper implements IAuthenticationWrapper { } public async logOutPopUp() { - const endSessionEndpoint = (await msalApplication.getDiscoveredAuthority()).endSessionEndpoint; + const endSessionEndpoint = (await msalApplication.getDiscoveredAuthority()) + .endSessionEndpoint; (window as any).open(endSessionEndpoint, 'msal', 400, 600); this.clearCache(); this.deleteHomeAccountId(); } /** - * Generates a new access token from passed in scopes - * @param {string[]} scopes passed to generate token - * @returns {Promise.} - */ + * Generates a new access token from passed in scopes + * @param {string[]} scopes passed to generate token + * @returns {Promise.} + */ public async consentToScopes(scopes: string[] = []): Promise { this.consentingToNewScopes = true; try { const authResult = await this.loginWithInteraction(scopes); + return authResult; } catch (error) { throw error; @@ -81,30 +89,33 @@ export class AuthenticationWrapper implements IAuthenticationWrapper { if (allAccounts.length > 1) { const homeAccountId = this.getHomeAccountId(); - return (homeAccountId) ? msalApplication.getAccountByHomeId(homeAccountId) || undefined : undefined; + return homeAccountId + ? msalApplication.getAccountByHomeId(homeAccountId) || undefined + : undefined; } return allAccounts[0]; } public async getToken() { - const silentRequest: SilentRequest = { - scopes: defaultScopes, - authority: this.getAuthority(), - account: this.getAccount() - }; + const silentRequest: SilentRequest = {scopes: defaultScopes,authority: this.getAuthority(), + account: this.getAccount(),redirectUri: getCurrentUri()}; try { - return await msalApplication.acquireTokenSilent(silentRequest); + const response: AuthenticationResult = + await msalApplication.acquireTokenSilent(silentRequest); + return response; } catch (error) { + throw error; } } private async getAuthResult(scopes: string[] = [], sessionId?: string): Promise { const silentRequest: SilentRequest = { - scopes: (scopes.length > 0) ? scopes : defaultScopes, + scopes: scopes.length > 0 ? scopes : defaultScopes, authority: this.getAuthority(), - account: this.getAccount() + account: this.getAccount(), + redirectUri: getCurrentUri() }; try { @@ -113,8 +124,14 @@ export class AuthenticationWrapper implements IAuthenticationWrapper { return result; } catch (error) { if (error instanceof InteractionRequiredAuthError || !this.getAccount()) { + return this.loginWithInteraction(silentRequest.scopes, sessionId); - } else { + + } else if(signInAuthError(error)) { + this.deleteHomeAccountId(); + throw error; + } + else{ throw error; } } @@ -138,7 +155,7 @@ export class AuthenticationWrapper implements IAuthenticationWrapper { authority: this.getAuthority(), prompt: 'select_account', redirectUri: getCurrentUri(), - extraQueryParameters: { mkt: geLocale } + extraQueryParameters: { mkt: geLocale }, }; if (this.consentingToNewScopes) { @@ -155,8 +172,20 @@ export class AuthenticationWrapper implements IAuthenticationWrapper { const result = await msalApplication.loginPopup(popUpRequest); this.storeHomeAccountId(result.account!); return result; - } catch (error) { - throw error; + }catch (error) { + const { errorCode } = error; + if( signInAuthError(errorCode) && !this.consentingToNewScopes ) { + this.clearSession(); + + if(errorCode === 'interaction_in_progress'){ + this.eraseInteractionInProgressCookie(); + } + throw error; + } + else{ + throw error; + } + } } @@ -168,7 +197,7 @@ export class AuthenticationWrapper implements IAuthenticationWrapper { return localStorage.getItem(HOME_ACCOUNT_KEY); } - private deleteHomeAccountId(): void { + public deleteHomeAccountId(): void { localStorage.removeItem(HOME_ACCOUNT_KEY); } @@ -181,12 +210,42 @@ export class AuthenticationWrapper implements IAuthenticationWrapper { * and uses either the homeAccountId 'login' to get localstorage keys that contain this * identifier */ - private clearCache(): void { + public clearCache(): void { const keyFilter = this.getHomeAccountId() || 'login'; - const msalKeys = Object.keys(localStorage).filter(key => key.includes(keyFilter)); + const msalKeys = Object.keys(localStorage).filter((key) => + key.includes(keyFilter) + ); msalKeys.forEach((item: string) => { localStorage.removeItem(item); }); } + private eraseInteractionInProgressCookie(): void{ + const keyValuePairs = document.cookie.split(';'); + let cookieValue = ''; + let cookieKey = ''; + + for( const pair of keyValuePairs){ + cookieValue = pair.substring(pair.indexOf('=')+1); + if(cookieValue === 'interaction_in_progress'){ + cookieKey = pair.substring(1, pair.indexOf('=')); + break; + } + } + this.createCookie(cookieKey,"",-100); + } + + private createCookie(name : string,value:string,days:number) : void { + let expires = '' + const date = new Date(); + date.setTime(date.getTime()+(days*24*60*60*1000)); + expires = "; expires="+date.toUTCString(); + document.cookie = name+"="+value+expires+"; path=/"; + } + + public clearSession(): void{ + this.clearCache(); + this.deleteHomeAccountId(); + window.sessionStorage.clear(); + } } diff --git a/src/modules/authentication/authUtils.ts b/src/modules/authentication/authUtils.ts index 5bd02ae9c..aa364a893 100644 --- a/src/modules/authentication/authUtils.ts +++ b/src/modules/authentication/authUtils.ts @@ -21,5 +21,5 @@ export function getLoginType(): LoginType { */ export function getCurrentUri(): string { const currentUrl = window.location.href.split('?')[0].split('#')[0]; - return currentUrl.toLowerCase(); + return currentUrl.toLowerCase() ; } diff --git a/src/types/status.ts b/src/types/status.ts index 20be4a914..88e96295e 100644 --- a/src/types/status.ts +++ b/src/types/status.ts @@ -6,4 +6,5 @@ export interface IStatus { status: number; statusText: string; duration?: number; + hint?: string; } \ No newline at end of file