diff --git a/components/UserDropdown.tsx b/components/UserDropdown.tsx index a42bdd93..9acf44f5 100644 --- a/components/UserDropdown.tsx +++ b/components/UserDropdown.tsx @@ -6,6 +6,21 @@ import { useTheme } from 'emotion-theming'; import defaultTheme from './theme'; import { Avatar, ChevronDown } from './theme/icons'; import useAuthContext from '../global/hooks/useAuthContext'; +import { UserWithId } from '../global/types'; + +const getDisplayName = (user?: UserWithId) => { + const greeting = 'Hello'; + if (user) { + if (user.firstName) { + return `${greeting}, ${user.firstName}`; + } else if (user.lastName) { + return `${greeting}, ${user.lastName}`; + } else if (user.email) { + return `${greeting}, ${user.email}`; + } + } + return greeting; +}; const CurrentUser = () => { const { user } = useAuthContext(); @@ -30,7 +45,7 @@ const CurrentUser = () => { max-width: 142px; `} > - Hello, {user?.firstName} + {getDisplayName(user)} ); diff --git a/global/hooks/useAuthContext.tsx b/global/hooks/useAuthContext.tsx index 00009936..5a9ff755 100644 --- a/global/hooks/useAuthContext.tsx +++ b/global/hooks/useAuthContext.tsx @@ -2,7 +2,7 @@ import React, { createContext, useState } from 'react'; import { useRouter } from 'next/router'; import { EGO_JWT_KEY } from '../utils/constants'; -import { decodeToken, extractUser, getPermissionsFromToken } from '../utils/egoTokenUtils'; +import { decodeToken, extractUser, isValidJwt } from '../utils/egoTokenUtils'; import { UserWithId } from '../../global/types'; type T_AuthContext = { @@ -45,18 +45,20 @@ export const AuthProvider = ({ setTokenState(egoJwt); } - const fetchWithAuth = (url: string, options: any) => { + if (token && !isValidJwt(token)) { + logout(); + } + + const fetchWithAuth: T_AuthContext['fetchWithAuth'] = (url, options) => { return fetch(url, { ...options, - headers: { ...options.headers, accept: '*/*', Authorization: `Bearer ${token || ''}` }, + headers: { ...options?.headers, accept: '*/*', Authorization: `Bearer ${token || ''}` }, body: null, }); }; const userInfo = token ? decodeToken(token) : null; - // ts error on userInfo from type discrepancy between dms and ego-token-utils user.preferredLanguage - // dms will need to use token-utils version updated for ego 4.x.x - const user = userInfo ? extractUser(userInfo) : {}; + const user = userInfo ? extractUser(userInfo) : undefined; const authData = { token, logout, diff --git a/global/utils/egoTokenUtils.ts b/global/utils/egoTokenUtils.ts index e77c91f8..fa21c756 100644 --- a/global/utils/egoTokenUtils.ts +++ b/global/utils/egoTokenUtils.ts @@ -1,20 +1,35 @@ -import createEgoUtils from '@icgc-argo/ego-token-utils'; +import jwtDecode from 'jwt-decode'; import { memoize } from 'lodash'; +import jwt from 'jsonwebtoken'; import { getConfig } from '../config'; import { EgoJwtData, UserWithId } from '../types'; -const TokenUtils = createEgoUtils(getConfig().NEXT_PUBLIC_EGO_PUBLIC_KEY); +const { NEXT_PUBLIC_EGO_PUBLIC_KEY } = getConfig(); -export const isValidJwt = (egoJwt: string | undefined) => !!egoJwt && TokenUtils.isValidJwt(egoJwt); +const verifyJwt: (egoPublicKey: string) => (egoJwt?: string) => boolean = (egoPublicKey) => ( + egoJwt, +) => { + try { + if (!egoJwt || !egoPublicKey) { + return false; + } else { + return jwt.verify(egoJwt, egoPublicKey, { algorithms: ['RS256'] }) && true; + } + } catch (err) { + return false; + } +}; + +export const isValidJwt = verifyJwt(NEXT_PUBLIC_EGO_PUBLIC_KEY); -export const decodeToken = memoize((egoJwt?: string) => - egoJwt ? TokenUtils.decodeToken(egoJwt) : null, +export const decodeToken: (egoJwt?: string) => EgoJwtData | null = memoize((egoJwt) => + egoJwt && isValidJwt(egoJwt) ? jwtDecode(egoJwt) : null, ); -export const extractUser: (decodedToken: EgoJwtData) => UserWithId | {} = (decodedToken) => { +export const extractUser: (decodedToken: EgoJwtData) => UserWithId | undefined = (decodedToken) => { if (decodedToken) { return { ...decodedToken?.context.user, id: decodedToken?.sub }; } - return {}; + return undefined; }; diff --git a/next-env.d.ts b/next-env.d.ts index 96f65894..c6c4e86f 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -3,3 +3,4 @@ declare module 'url-join'; declare module 'js-cookie'; +declare module 'jsonwebtoken'; diff --git a/package-lock.json b/package-lock.json index 62a9e5ad..36500d34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1708,14 +1708,6 @@ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.1.1.tgz", "integrity": "sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw==" }, - "@icgc-argo/ego-token-utils": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@icgc-argo/ego-token-utils/-/ego-token-utils-8.0.0.tgz", - "integrity": "sha512-sgBHslKS3TnSSCNcdOpwrophLRrZitVpnussv4iRD0VKn6OFCfR32AXn+Ri9LF6KifS3FsE1WtvAYNJx1Ewz7g==", - "requires": { - "jsonwebtoken": "^8.5.1" - } - }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -8421,6 +8413,11 @@ "safe-buffer": "^5.0.1" } }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", diff --git a/package.json b/package.json index 70b59ac7..5aec78ef 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,12 @@ "@emotion/babel-preset-css-prop": "^10.0.27", "@emotion/core": "^10.0.35", "@emotion/styled": "^10.0.27", - "@icgc-argo/ego-token-utils": "^8.0.0", "@types/lodash": "^4.14.168", "@zeit/next-css": "^1.0.1", "axios": "^0.21.1", "emotion-theming": "^10.0.27", + "jsonwebtoken": "^8.5.1", + "jwt-decode": "^3.1.2", "lodash": "^4.17.20", "next": "^9.5.4", "query-string": "^6.13.8",