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