feat: create token exchange service to hitphone integration and microsoft token validation
parent
975f7504aa
commit
9fd234667d
|
@ -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<string> => {
|
||||||
|
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 }
|
||||||
|
};
|
|
@ -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<TokenVerifierResponse> => {
|
||||||
|
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<TokenVerifierResponse> => {
|
||||||
|
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<TokenVerifierResponse>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TokenExchangeServiceResponse = {
|
||||||
|
token: string;
|
||||||
|
refreshToken: string;
|
||||||
|
serializedUser: SerializedUser
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TokenExchangeService = async (
|
||||||
|
{
|
||||||
|
token,
|
||||||
|
tokenVerifier
|
||||||
|
}: TokenExchangeServiceRequest): Promise<TokenExchangeServiceResponse> => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue