You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

functions.ts 7.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. // @ts-expect-error
  2. import jwtDecode from 'jwt-decode';
  3. import { IReduxState } from '../../app/types';
  4. import { isVpaasMeeting } from '../../jaas/functions';
  5. import { getLocalParticipant } from '../participants/functions';
  6. import { IParticipantFeatures } from '../participants/types';
  7. import { parseURLParams } from '../util/parseURLParams';
  8. import { JWT_VALIDATION_ERRORS, MEET_FEATURES } from './constants';
  9. import logger from './logger';
  10. /**
  11. * Retrieves the JSON Web Token (JWT), if any, defined by a specific
  12. * {@link URL}.
  13. *
  14. * @param {URL} url - The {@code URL} to parse and retrieve the JSON Web Token
  15. * (JWT), if any, from.
  16. * @returns {string} The JSON Web Token (JWT), if any, defined by the specified
  17. * {@code url}; otherwise, {@code undefined}.
  18. */
  19. export function parseJWTFromURLParams(url: URL | typeof window.location = window.location) {
  20. // @ts-ignore
  21. const jwt = parseURLParams(url, false, 'hash').jwt;
  22. // TODO: eventually remove the search param and only pull from the hash
  23. // @ts-ignore
  24. return jwt ? jwt : parseURLParams(url, true, 'search').jwt;
  25. }
  26. /**
  27. * Returns the user name after decoding the jwt.
  28. *
  29. * @param {IReduxState} state - The app state.
  30. * @returns {string}
  31. */
  32. export function getJwtName(state: IReduxState) {
  33. const { user } = state['features/base/jwt'];
  34. return user?.name;
  35. }
  36. /**
  37. * Check if the given JWT feature is enabled.
  38. *
  39. * @param {IReduxState} state - The app state.
  40. * @param {string} feature - The feature we want to check.
  41. * @param {boolean} ifNoToken - Default value if there is no token.
  42. * @param {boolean} ifNotInFeatures - Default value if features prop exists but does not have the {@code feature}.
  43. * @returns {boolean}
  44. */
  45. export function isJwtFeatureEnabled(state: IReduxState, feature: string, ifNoToken: boolean, ifNotInFeatures: boolean) {
  46. const { jwt } = state['features/base/jwt'];
  47. let { features } = getLocalParticipant(state) || {};
  48. if (typeof features === 'undefined' && isVpaasMeeting(state)) {
  49. // for vpaas the backend is always initialized with empty features if those are missing
  50. features = {};
  51. }
  52. return isJwtFeatureEnabledStateless({
  53. jwt,
  54. localParticipantFeatures: features,
  55. feature,
  56. ifNoToken,
  57. ifNotInFeatures
  58. });
  59. }
  60. interface IIsJwtFeatureEnabledStatelessParams {
  61. feature: string;
  62. ifNoToken: boolean;
  63. ifNotInFeatures: boolean;
  64. jwt?: string;
  65. localParticipantFeatures?: IParticipantFeatures;
  66. }
  67. /**
  68. * Check if the given JWT feature is enabled.
  69. *
  70. * @param {string | undefined} jwt - The jwt token.
  71. * @param {ILocalParticipant} localParticipantFeatures - The features of the local participant.
  72. * @param {string} feature - The feature we want to check.
  73. * @param {boolean} ifNoToken - Default value if there is no token.
  74. * @param {boolean} ifNotInFeatures - Default value if features is missing
  75. * or prop exists but does not have the {@code feature}.
  76. * @returns {boolean}
  77. */
  78. export function isJwtFeatureEnabledStateless({
  79. jwt,
  80. localParticipantFeatures: features,
  81. feature,
  82. ifNoToken,
  83. ifNotInFeatures
  84. }: IIsJwtFeatureEnabledStatelessParams) {
  85. if (!jwt) {
  86. return ifNoToken;
  87. }
  88. if (typeof features === 'undefined') {
  89. return ifNoToken;
  90. }
  91. if (typeof features[feature as keyof typeof features] === 'undefined') {
  92. return ifNotInFeatures;
  93. }
  94. return String(features[feature as keyof typeof features]) === 'true';
  95. }
  96. /**
  97. * Checks whether a given timestamp is a valid UNIX timestamp in seconds.
  98. * We convert to milliseconds during the check since `Date` works with milliseconds for UNIX timestamp values.
  99. *
  100. * @param {any} timestamp - A UNIX timestamp in seconds as stored in the jwt.
  101. * @returns {boolean} - Whether the timestamp is indeed a valid UNIX timestamp or not.
  102. */
  103. function isValidUnixTimestamp(timestamp: number | string) {
  104. return typeof timestamp === 'number' && timestamp * 1000 === new Date(timestamp * 1000).getTime();
  105. }
  106. /**
  107. * Returns a list with all validation errors for the given jwt.
  108. *
  109. * @param {string} jwt - The jwt.
  110. * @returns {Array} - An array containing all jwt validation errors.
  111. */
  112. export function validateJwt(jwt: string) {
  113. const errors: Object[] = [];
  114. const currentTimestamp = new Date().getTime();
  115. try {
  116. const header = jwtDecode(jwt, { header: true });
  117. const payload = jwtDecode(jwt);
  118. if (!header) {
  119. errors.push({ key: JWT_VALIDATION_ERRORS.HEADER_NOT_FOUND });
  120. return errors;
  121. }
  122. if (!payload) {
  123. errors.push({ key: JWT_VALIDATION_ERRORS.PAYLOAD_NOT_FOUND });
  124. return errors;
  125. }
  126. const {
  127. aud,
  128. context,
  129. exp,
  130. iss,
  131. nbf,
  132. sub
  133. } = payload;
  134. // JaaS only
  135. if (sub?.startsWith('vpaas-magic-cookie')) {
  136. const { kid } = header;
  137. // if Key ID is missing, we return the error immediately without further validations.
  138. if (!kid) {
  139. errors.push({ key: JWT_VALIDATION_ERRORS.KID_NOT_FOUND });
  140. return errors;
  141. }
  142. if (kid.substring(0, kid.indexOf('/')) !== sub) {
  143. errors.push({ key: JWT_VALIDATION_ERRORS.KID_MISMATCH });
  144. }
  145. if (aud !== 'jitsi') {
  146. errors.push({ key: JWT_VALIDATION_ERRORS.AUD_INVALID });
  147. }
  148. if (iss !== 'chat') {
  149. errors.push({ key: JWT_VALIDATION_ERRORS.ISS_INVALID });
  150. }
  151. if (!context?.features) {
  152. errors.push({ key: JWT_VALIDATION_ERRORS.FEATURES_NOT_FOUND });
  153. }
  154. }
  155. if (nbf) { // nbf value is optional
  156. if (!isValidUnixTimestamp(nbf)) {
  157. errors.push({ key: JWT_VALIDATION_ERRORS.NBF_INVALID });
  158. } else if (currentTimestamp < nbf * 1000) {
  159. errors.push({ key: JWT_VALIDATION_ERRORS.NBF_FUTURE });
  160. }
  161. }
  162. if (!isValidUnixTimestamp(exp)) {
  163. errors.push({ key: JWT_VALIDATION_ERRORS.EXP_INVALID });
  164. } else if (currentTimestamp > exp * 1000) {
  165. errors.push({ key: JWT_VALIDATION_ERRORS.TOKEN_EXPIRED });
  166. }
  167. if (!context) {
  168. errors.push({ key: JWT_VALIDATION_ERRORS.CONTEXT_NOT_FOUND });
  169. } else if (context.features) {
  170. const { features } = context;
  171. const meetFeatures = Object.values(MEET_FEATURES);
  172. Object.keys(features).forEach(feature => {
  173. if (meetFeatures.includes(feature)) {
  174. const featureValue = features[feature];
  175. // cannot use truthy or falsy because we need the exact value and type check.
  176. if (
  177. featureValue !== true
  178. && featureValue !== false
  179. && featureValue !== 'true'
  180. && featureValue !== 'false'
  181. ) {
  182. errors.push({
  183. key: JWT_VALIDATION_ERRORS.FEATURE_VALUE_INVALID,
  184. args: { feature }
  185. });
  186. }
  187. } else {
  188. errors.push({
  189. key: JWT_VALIDATION_ERRORS.FEATURE_INVALID,
  190. args: { feature }
  191. });
  192. }
  193. });
  194. }
  195. } catch (e: any) {
  196. logger.error(`Unspecified JWT error${e?.message ? `: ${e.message}` : ''}`);
  197. }
  198. return errors;
  199. }
  200. /**
  201. * Extracts and returns the expiration date of jwt.
  202. *
  203. * @param {string|undefined} jwt - The jwt to check.
  204. * @returns {Date} The expiration date of the jwt.
  205. */
  206. export function getJwtExpirationDate(jwt: string | undefined) {
  207. if (!jwt) {
  208. return;
  209. }
  210. const payload = jwtDecode(jwt);
  211. if (payload) {
  212. const { exp } = payload;
  213. return new Date(exp * 1000);
  214. }
  215. }