feat(jwt) enhance JWT error notifications with details (#13668)

pull/13681/head jitsi-meet_8881
Mihaela Dumitru 2 years ago committed by GitHub
parent 1066c65a6a
commit f75ae6bd21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      config.js
  2. 17
      lang/main.json
  3. 46
      react/features/base/conference/middleware.any.ts
  4. 20
      react/features/base/jwt/constants.ts
  5. 54
      react/features/base/jwt/functions.ts
  6. 1
      react/features/notifications/components/web/Notification.tsx
  7. 1
      react/features/notifications/reducer.ts

@ -1511,6 +1511,7 @@ var config = {
// 'dialog.sessTerminated', // shown when there is a failed conference session
// 'dialog.sessionRestarted', // show when a client reload is initiated because of bridge migration
// 'dialog.tokenAuthFailed', // show when an invalid jwt is used
// 'dialog.tokenAuthFailedWithReasons', // show when an invalid jwt is used with the reason behind the error
// 'dialog.transcribing', // transcribing notifications (pending, off)
// 'dialOut.statusMessage', // shown when dial out status is updated.
// 'liveStreaming.busy', // shown when livestreaming service is busy

@ -440,7 +440,24 @@
"thankYou": "Thank you for using {{appName}}!",
"token": "token",
"tokenAuthFailed": "Sorry, you're not allowed to join this call.",
"tokenAuthFailedReason": {
"audInvalid": "Ivalid `aud` value. It should be `jitsi`.",
"contextNotFound": "The `context` object is missing from the payload.",
"expInvalid": "Invalid `exp` value.",
"featureInvalid": "Invalid feature: {{feature}}, most likely not implemented yet.",
"featureValueInvalid": "Invalid value for feature: {{feature}}.",
"featuresNotFound": "The `features` object is missing from the payload.",
"headerNotFound": "Missing the header.",
"issInvalid": "Invalid `iss` value. It should be `chat`.",
"kidMismatch": "Key ID (kid) does not match sub.",
"kidNotFound": "Missing Key ID (kid).",
"nbfFuture": "The `nbf` value is in the future.",
"nbfInvalid": "Invalid `nbf` value.",
"payloadNotFound": "Missing the payload.",
"tokenExpired": "Token is expired."
},
"tokenAuthFailedTitle": "Authentication failed",
"tokenAuthFailedWithReasons": "Sorry, you're not allowed to join this call. Possible reasons: {{reason}}",
"tokenAuthUnsupported": "Token URL is not supported.",
"transcribing": "Transcribing",
"unlockRoom": "Remove meeting $t(lockRoomPassword)",

@ -1,3 +1,4 @@
import i18n from 'i18next';
import { AnyAction } from 'redux';
// @ts-ignore
@ -10,7 +11,7 @@ import {
} from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { reloadNow } from '../../app/actions';
import { IReduxState, IStore } from '../../app/types';
import { IStore } from '../../app/types';
import { removeLobbyChatParticipant } from '../../chat/actions.any';
import { openDisplayNamePrompt } from '../../display-name/actions';
import { readyToClose } from '../../mobile/external-api/actions';
@ -383,21 +384,13 @@ async function _connectionEstablished({ dispatch, getState }: IStore, next: Func
/**
* Logs jwt validation errors from xmpp and from the client-side validator.
*
* @param {string} message -The error message from xmpp.
* @param {Object} state - The redux state.
* @param {string} message - The error message from xmpp.
* @param {string} errors - The detailed errors.
* @returns {void}
*/
function _logJwtErrors(message: string, state: IReduxState) {
const { jwt } = state['features/base/jwt'];
if (!jwt) {
return;
}
const errorKeys = validateJwt(jwt);
function _logJwtErrors(message: string, errors: string) {
message && logger.error(`JWT error: ${message}`);
errorKeys.length && logger.error('JWT parsing error:', errorKeys);
errors && logger.error('JWT parsing errors:', errors);
}
/**
@ -415,18 +408,25 @@ function _logJwtErrors(message: string, state: IReduxState) {
* @returns {Object} The value returned by {@code next(action)}.
*/
function _connectionFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
_logJwtErrors(action.error.message, getState());
const { connection, error } = action;
const { jwt } = getState()['features/base/jwt'];
// do not show the notification when we will prompt the user
// for username and password
if (error.name === JitsiConnectionErrors.PASSWORD_REQUIRED
&& getState()['features/base/jwt'].jwt) {
dispatch(showErrorNotification({
descriptionKey: 'dialog.tokenAuthFailed',
titleKey: 'dialog.tokenAuthFailedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
if (jwt) {
const errors: string = validateJwt(jwt).map((err: any) =>
i18n.t(`dialog.tokenAuthFailedReason.${err.key}`, err.args))
.join(' ');
_logJwtErrors(error.message, errors);
// do not show the notification when we will prompt the user
// for username and password
if (error.name === JitsiConnectionErrors.PASSWORD_REQUIRED) {
dispatch(showErrorNotification({
descriptionKey: errors ? 'dialog.tokenAuthFailedWithReasons' : 'dialog.tokenAuthFailed',
descriptionArguments: { reason: errors },
titleKey: 'dialog.tokenAuthFailedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}
}
const result = next(action);

@ -27,3 +27,23 @@ export const FEATURES_TO_BUTTONS_MAPPING = {
'recording': 'recording',
'transcription': 'closedcaptions'
};
/**
* The JWT validation errors for JaaS.
*/
export const JWT_VALIDATION_ERRORS = {
AUD_INVALID: 'audInvalid',
CONTEXT_NOT_FOUND: 'contextNotFound',
EXP_INVALID: 'expInvalid',
FEATURE_INVALID: 'featureInvalid',
FEATURE_VALUE_INVALID: 'featureValueInvalid',
FEATURES_NOT_FOUND: 'featuresNotFound',
HEADER_NOT_FOUND: 'headerNotFound',
ISS_INVALID: 'issInvalid',
KID_NOT_FOUND: 'kidNotFound',
KID_MISMATCH: 'kidMismatch',
NBF_FUTURE: 'nbfFuture',
NBF_INVALID: 'nbfInvalid',
PAYLOAD_NOT_FOUND: 'payloadNotFound',
TOKEN_EXPIRED: 'tokenExpired'
};

@ -5,7 +5,8 @@ import { IReduxState } from '../../app/types';
import { getLocalParticipant } from '../participants/functions';
import { parseURLParams } from '../util/parseURLParams';
import { MEET_FEATURES } from './constants';
import { JWT_VALIDATION_ERRORS, MEET_FEATURES } from './constants';
import logger from './logger';
/**
* Retrieves the JSON Web Token (JWT), if any, defined by a specific
@ -78,23 +79,24 @@ function isValidUnixTimestamp(timestamp: number | string) {
* Returns a list with all validation errors for the given jwt.
*
* @param {string} jwt - The jwt.
* @returns {Array<string>} - An array containing all jwt validation errors.
* @returns {Array} - An array containing all jwt validation errors.
*/
export function validateJwt(jwt: string) {
const errors: string[] = [];
if (!jwt) {
return errors;
}
const errors: Object[] = [];
const currentTimestamp = new Date().getTime();
try {
const header = jwtDecode(jwt, { header: true });
const payload = jwtDecode(jwt);
if (!header || !payload) {
errors.push('- Missing header or payload');
if (!header) {
errors.push({ key: JWT_VALIDATION_ERRORS.HEADER_NOT_FOUND });
return errors;
}
if (!payload) {
errors.push({ key: JWT_VALIDATION_ERRORS.PAYLOAD_NOT_FOUND });
return errors;
}
@ -114,42 +116,42 @@ export function validateJwt(jwt: string) {
// if Key ID is missing, we return the error immediately without further validations.
if (!kid) {
errors.push('- Key ID(kid) missing');
errors.push({ key: JWT_VALIDATION_ERRORS.KID_NOT_FOUND });
return errors;
}
if (kid.substring(0, kid.indexOf('/')) !== sub) {
errors.push('- Key ID(kid) does not match sub');
errors.push({ key: JWT_VALIDATION_ERRORS.KID_MISMATCH });
}
if (aud !== 'jitsi') {
errors.push('- invalid `aud` value. It should be `jitsi`');
errors.push({ key: JWT_VALIDATION_ERRORS.AUD_INVALID });
}
if (iss !== 'chat') {
errors.push('- invalid `iss` value. It should be `chat`');
errors.push({ key: JWT_VALIDATION_ERRORS.ISS_INVALID });
}
if (!context?.features) {
errors.push('- `features` object is missing from the payload');
errors.push({ key: JWT_VALIDATION_ERRORS.FEATURES_NOT_FOUND });
}
}
if (!isValidUnixTimestamp(nbf)) {
errors.push('- invalid `nbf` value');
errors.push({ key: JWT_VALIDATION_ERRORS.NBF_INVALID });
} else if (currentTimestamp < nbf * 1000) {
errors.push('- `nbf` value is in the future');
errors.push({ key: JWT_VALIDATION_ERRORS.NBF_FUTURE });
}
if (!isValidUnixTimestamp(exp)) {
errors.push('- invalid `exp` value');
errors.push({ key: JWT_VALIDATION_ERRORS.EXP_INVALID });
} else if (currentTimestamp > exp * 1000) {
errors.push('- token is expired');
errors.push({ key: JWT_VALIDATION_ERRORS.TOKEN_EXPIRED });
}
if (!context) {
errors.push('- `context` object is missing from the payload');
errors.push({ key: JWT_VALIDATION_ERRORS.CONTEXT_NOT_FOUND });
} else if (context.features) {
const { features } = context;
const meetFeatures = Object.values(MEET_FEATURES);
@ -165,15 +167,21 @@ export function validateJwt(jwt: string) {
&& featureValue !== 'true'
&& featureValue !== 'false'
) {
errors.push(`- Invalid value for feature: ${feature}`);
errors.push({
key: JWT_VALIDATION_ERRORS.FEATURE_VALUE_INVALID,
args: { feature }
});
}
} else {
errors.push(`- Invalid feature: ${feature}`);
errors.push({
key: JWT_VALIDATION_ERRORS.FEATURE_INVALID,
args: { feature }
});
}
});
}
} catch (e: any) {
errors.push(e ? e.message : '- unspecified jwt error');
logger.error(`Unspecified JWT error${e?.message ? `: ${e.message}` : ''}`);
}
return errors;

@ -129,6 +129,7 @@ const useStyles = makeStyles()((theme: Theme) => {
...withPixelLineHeight(theme.typography.bodyShortRegular),
overflow: 'auto',
overflowWrap: 'break-word',
userSelect: 'all',
'&:not(:empty)': {
marginTop: theme.spacing(1)

@ -22,6 +22,7 @@ interface INotification {
component: Object;
props: {
appearance?: string;
descriptionArguments?: Object;
descriptionKey?: string;
titleKey: string;
};

Loading…
Cancel
Save