From 9fd234667d4c1e3b4ab447ce924f0edef21e6760 Mon Sep 17 00:00:00 2001 From: Henrriky Date: Wed, 8 May 2024 12:29:19 -0300 Subject: [PATCH] feat: create token exchange service to hitphone integration and microsoft token validation --- backend/src/libs/teamsTokenValidation.ts | 68 +++++++++++ .../AuthServices/TokenExchangeService.ts | 106 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 backend/src/libs/teamsTokenValidation.ts create mode 100644 backend/src/services/AuthServices/TokenExchangeService.ts diff --git a/backend/src/libs/teamsTokenValidation.ts b/backend/src/libs/teamsTokenValidation.ts new file mode 100644 index 0000000..e27f97f --- /dev/null +++ b/backend/src/libs/teamsTokenValidation.ts @@ -0,0 +1,68 @@ +import jwt, { JwtPayload } from "jsonwebtoken"; +import JwksRsa from "jwks-rsa"; + +interface Token { + aud: string; + iss: string; + sub: string; + preferred_username: string; +} + +const getTokenKid = (token: string) => { + const headers = token.split(".")[0]; + + if (!headers) { + throw new Error("No headers"); + } + + const buffer = Buffer.from(headers, "base64"); + const text = buffer.toString("ascii"); + + const kid = JSON.parse(text).kid; + + if (!kid) { + throw new Error("No kid"); + } + + if (typeof kid !== "string") { + throw new Error("Invalid kid"); + } + + return kid; +} + +const getTokenPublicKey = async (token: string): Promise => { + const kid = getTokenKid(token); + + const publicKey = ( + await JwksRsa({ + cache: true, + rateLimit: true, + jwksUri: "https://login.microsoftonline.com/common/discovery/keys", + }).getSigningKey(kid) + ).getPublicKey(); + + return publicKey; +}; + +export const verifyTeamsToken = async (token: string): Promise<{ payload: Token & JwtPayload }> => { + let publicKey; + + try { + publicKey = await getTokenPublicKey(token); + } catch (error) { + throw new Error( + `Error getting publicKey to verify token\nReason: ${error}` + ); + } + + const decoded = jwt.verify( + token, + publicKey, + { + algorithms: ["RS256"], + } + ) as Token; + + return { payload: decoded } +}; \ No newline at end of file diff --git a/backend/src/services/AuthServices/TokenExchangeService.ts b/backend/src/services/AuthServices/TokenExchangeService.ts new file mode 100644 index 0000000..5ba6c41 --- /dev/null +++ b/backend/src/services/AuthServices/TokenExchangeService.ts @@ -0,0 +1,106 @@ +import { verify, Algorithm, JwtPayload } from "jsonwebtoken"; +import AppError from "../../errors/AppError"; +import authConfig from "../../config/auth"; +import { + createAccessToken, + createRefreshToken +} from "../../helpers/CreateTokens"; +import User from "../../models/User"; +import { SerializeUser, SerializedUser } from "../../helpers/SerializeUser"; +import { verifyTeamsToken } from "../../libs/teamsTokenValidation"; +import { clientExists } from "../External/HitphoneServices/ClientExists"; + +type TokenVerifierResponse = { + name: string; + email: string; + sub: string; +} + +export const verifyTokenFromWebService = async (token: string): Promise => { + const userPayload = verify( + token, + authConfig.hitphone.jwtPublicKey, + { + algorithms: [authConfig.hitphone.jwtAlgorithm as Algorithm], + audience: authConfig.hitphone.jwtAudience, + issuer: authConfig.hitphone.jwtIssuer, + } + ) as JwtPayload; + + const requiredFields = ["email"]; + + for (const key of requiredFields) { + if (!userPayload[key]) { + throw new AppError(`ERR_TOKEN_${key.toUpperCase()}_INVALID`, 401) + } + } + + return { name: userPayload.email, email: userPayload.email, sub: userPayload.sub! } +} + +export const verifyTokenFromTeamsService = async (token: string): Promise => { + const { payload: userPayload } = await verifyTeamsToken(token) + + if (userPayload.aud !== authConfig.hitphone.teams.CLIENT_ID) { + throw new AppError("ERR_TOKEN_AUD_INVALID", 401); + } + + const requiredFields = ["aud", "tid", "oid", "preferred_username"]; + for (const key of requiredFields) { + if (!userPayload[key]) { + throw new AppError(`ERR_TOKEN_${key.toUpperCase()}_INVALID`, 401) + } + } + + const exists = await clientExists(userPayload.tid); + + if (!exists) { + throw new AppError("ERR_CLIENT_NOT_FOUND", 401); + } + + return { name: userPayload.preferred_username, email: userPayload.preferred_username, sub: userPayload.sub } +} + +type TokenExchangeServiceRequest = { + token: string; + tokenVerifier: (token: string) => Promise +} + +export type TokenExchangeServiceResponse = { + token: string; + refreshToken: string; + serializedUser: SerializedUser +} + +export const TokenExchangeService = async ( + { + token, + tokenVerifier + }: TokenExchangeServiceRequest): Promise => { + try { + + const { email } = await tokenVerifier(token); + + const user = await User.findOne({ + where: { email }, + include: ["queues"] + }); + + if (!user) { + throw new AppError("ERR_INVALID_CREDENTIALS", 401); + } + + const generatedToken = createAccessToken(user); + const generatedRefreshToken = createRefreshToken(user); + const generatedSerializedUser = SerializeUser(user) + + return { + token: generatedToken, + refreshToken: generatedRefreshToken, + serializedUser: generatedSerializedUser, + } + } catch (err) { + console.error(err) + throw new AppError("ERR_INVALID_TOKEN", 401); + } +};