Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/appSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ export const appSettings = {
connectionString: process.env['communicationServices_connectionString'] || '<your_connection_string>',
scopes: ['voip', 'chat'] as TokenScope[]
},
azureActiveDirectory: {
microsoftEntraID: {
instance: 'https://login.microsoftonline.com',
clientId: process.env['azureActiveDirectory_clientId'] || '<your_client_id>', // Application (Client) ID from Overview of app registration from Azure Portal, e.g. 2ed40e05-ba00-4853-xxxx-xxx60029x596]
clientSecret: process.env['azureActiveDirectory_clientSecret'] || '<your_client_secret>', // Client secret from Overview of app registration from Azure Portal
tenantId: process.env['azureActiveDirectory_tenantId'] || '<your_tenant_id>' // Directory (Tenant) ID from Overview of app registration from Azure Portal, or 'common' or 'organizations' or 'consumers'
clientId: process.env['microsoftEntraID_clientId'] || '<your_client_id>', // Application (Client) ID from Overview of app registration from Azure Portal, e.g. 2ed40e05-ba00-4853-xxxx-xxx60029x596]
clientSecret: process.env['microsoftEntraID_clientSecret'] || '<your_client_secret>', // Client secret from Overview of app registration from Azure Portal
tenantId: process.env['microsoftEntraID_tenantId'] || '<your_tenant_id>' // Directory (Tenant) ID from Overview of app registration from Azure Portal, or 'common' or 'organizations' or 'consumers'
}
};
16 changes: 8 additions & 8 deletions src/controllers/tokenController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
*---------------------------------------------------------------------------------------------*/

import { NextFunction, Request, Response } from 'express';
import { createErrorResponse, getAADTokenViaRequest } from '../utils/utils';
import { createErrorResponse, getMEIDTokenViaRequest } from '../utils/utils';
import { getACSUserId } from '../services/graphService';
import { createACSToken, getACSTokenForTeamsUser } from '../services/acsService';
import { exchangeAADTokenViaOBO } from '../services/aadService';
import { exchangeMEIDTokenViaOBO } from '../services/aadService';
import { AuthenticatedRequest } from 'src/types/authenticatedRequest';

const ACS_IDENTITY_NOT_FOUND_ERROR = 'Can not find any ACS identities in Microsoft Graph used to create an ACS token';
Expand All @@ -24,12 +24,12 @@ const ACS_IDENTITY_NOT_FOUND_ERROR = 'Can not find any ACS identities in Microso
export const getACSToken = async (req: Request, res: Response, next: NextFunction) => {
try {
// Get aad token via the request
const aadTokenViaRequest = getAADTokenViaRequest(req);
const meidTokenViaRequest = getMEIDTokenViaRequest(req);
// Retrieve the AAD token via OBO flow
const aadTokenExchangedViaOBO = await exchangeAADTokenViaOBO(aadTokenViaRequest);
const meidTokenExchangedViaOBO = await exchangeMEIDTokenViaOBO(meidTokenViaRequest);

// Retrieve ACS Identity from Microsoft Graph
const acsUserId = await getACSUserId(aadTokenExchangedViaOBO);
const acsUserId = await getACSUserId(meidTokenExchangedViaOBO);

if (acsUserId !== undefined) {
// The ACS user exists
Expand All @@ -56,14 +56,14 @@ export const getACSToken = async (req: Request, res: Response, next: NextFunctio
* 2. Get Azure AD user object ID obtained from the oid claim of the token received in the Authorization header
* 3. Initialize a Communication Identity Client and then issue an ACS access token for the Teams user
*/
export const exchangeAADToken = async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
export const exchangeMEIDToken = async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
// Get an Azure AD token passed through the 'teams-user-aad-token' header
const teamsUserAadToken = req.headers['teams-user-aad-token'] as string;
const teamsUserMeidToken = req.headers['teams-user-meid-token'] as string;
// Get the oid claim of the token received in the Authorization header
const userObjectId = req.user.oid;
// Exchange the AAD user token for the Teams access token
const acsTokenForTeamsUser = await getACSTokenForTeamsUser(teamsUserAadToken, userObjectId);
const acsTokenForTeamsUser = await getACSTokenForTeamsUser(teamsUserMeidToken, userObjectId);
return res.status(201).json(acsTokenForTeamsUser);
} catch (error) {
next(error);
Expand Down
26 changes: 13 additions & 13 deletions src/controllers/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
*---------------------------------------------------------------------------------------------*/

import { NextFunction, Request, Response } from 'express';
import { createErrorResponse, getAADTokenViaRequest } from '../utils/utils';
import { exchangeAADTokenViaOBO } from '../services/aadService';
import { createErrorResponse, getMEIDTokenViaRequest } from '../utils/utils';
import { exchangeMEIDTokenViaOBO } from '../services/aadService';
import { createACSUserIdentity, deleteACSUserIdentity } from '../services/acsService';
import { addIdentityMapping, deleteIdentityMapping, getACSUserId } from '../services/graphService';

Expand All @@ -17,16 +17,16 @@ const NO_IDENTITY_MAPPING_INFO_ERROR = 'There is no identity mapping information
export const createACSUser = async (req: Request, res: Response, next: NextFunction) => {
try {
// Get aad token via the request
const aadTokenViaRequest = getAADTokenViaRequest(req);
const meidTokenViaRequest = getMEIDTokenViaRequest(req);
// Retrieve the AAD token via OBO flow
const aadTokenExchangedViaOBO = await exchangeAADTokenViaOBO(aadTokenViaRequest);
const meidTokenExchangedViaOBO = await exchangeMEIDTokenViaOBO(meidTokenViaRequest);
// Get an ACS user id from Microsoft Graph
let acsUserId = await getACSUserId(aadTokenExchangedViaOBO);
let acsUserId = await getACSUserId(meidTokenExchangedViaOBO);

if (acsUserId === undefined) {
// Create a Communication Services identity.
acsUserId = await createACSUserIdentity();
const identityMappingResponse = await addIdentityMapping(aadTokenExchangedViaOBO, acsUserId);
const identityMappingResponse = await addIdentityMapping(meidTokenExchangedViaOBO, acsUserId);
return res.status(201).json(identityMappingResponse);
}

Expand All @@ -42,11 +42,11 @@ export const createACSUser = async (req: Request, res: Response, next: NextFunct
export const getACSUser = async (req: Request, res: Response, next: NextFunction) => {
try {
// Get aad token via the request
const aadTokenViaRequest = getAADTokenViaRequest(req);
const meidTokenViaRequest = getMEIDTokenViaRequest(req);
// Retrieve the AAD token via OBO flow
const aadTokenExchangedViaOBO = await exchangeAADTokenViaOBO(aadTokenViaRequest);
const meidTokenExchangedViaOBO = await exchangeMEIDTokenViaOBO(meidTokenViaRequest);
// Get an ACS user id from Microsoft Graph
const acsUserId = await getACSUserId(aadTokenExchangedViaOBO);
const acsUserId = await getACSUserId(meidTokenExchangedViaOBO);

return acsUserId === undefined
? res.status(404).json(createErrorResponse(404, NO_IDENTITY_MAPPING_INFO_ERROR))
Expand All @@ -69,14 +69,14 @@ export const getACSUser = async (req: Request, res: Response, next: NextFunction
export const deleteACSUser = async (req: Request, res: Response, next: NextFunction) => {
try {
// Get aad token via the request
const aadTokenViaRequest = getAADTokenViaRequest(req);
const meidTokenViaRequest = getMEIDTokenViaRequest(req);
// Retrieve the AAD token via OBO flow
const aadTokenExchangedViaOBO = await exchangeAADTokenViaOBO(aadTokenViaRequest);
const meidTokenExchangedViaOBO = await exchangeMEIDTokenViaOBO(meidTokenViaRequest);
// Get an ACS user id from Microsoft Graph
const acsUserId = await getACSUserId(aadTokenExchangedViaOBO);
const acsUserId = await getACSUserId(meidTokenExchangedViaOBO);

// Delete the identity mapping from the user's roaming profile information using Microsoft Graph Open Extension
await deleteIdentityMapping(aadTokenExchangedViaOBO);
await deleteIdentityMapping(meidTokenExchangedViaOBO);
// Delete the ACS user identity which revokes all active access tokens
// and prevents users from issuing access tokens for the identity.
// It also removes all the persisted content associated with the identity.
Expand Down
4 changes: 2 additions & 2 deletions src/routes/tokenRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*---------------------------------------------------------------------------------------------*/

import express from 'express';
import { exchangeAADToken, getACSToken } from '../controllers/tokenController';
import { exchangeMEIDToken, getACSToken } from '../controllers/tokenController';
import { checkJwt, checkScope } from '../utils/utils';

export const tokenRouter = () => {
Expand All @@ -15,7 +15,7 @@ export const tokenRouter = () => {
// 1. Get an ACS token or refresh an ACS token
router.get('/', checkJwt, checkScope, getACSToken);
// 2. Get an ACS token for a Teams user
router.get('/teams', checkJwt, checkScope, exchangeAADToken);
router.get('/teams', checkJwt, checkScope, exchangeMEIDToken);

return router;
};
16 changes: 8 additions & 8 deletions src/services/aadService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Configuration, ConfidentialClientApplication } from '@azure/msal-node';
import { appSettings } from '../appSettings';

// Error messages
const EXCHANGE_AAD_TOKEN_VIA_OBO_ERROR =
const EXCHANGE_MEID_TOKEN_VIA_OBO_ERROR =
'An error occurred when exchanging the incoming access token for another access token to call downstream APIs through On-Behalf-Of flow';

/**
Expand All @@ -18,9 +18,9 @@ const EXCHANGE_AAD_TOKEN_VIA_OBO_ERROR =
export const createConfidentialClientApplication = (): ConfidentialClientApplication => {
const msalConfig: Configuration = {
auth: {
clientId: appSettings.azureActiveDirectory.clientId,
authority: `${appSettings.azureActiveDirectory.instance}/${appSettings.azureActiveDirectory.tenantId}`,
clientSecret: appSettings.azureActiveDirectory.clientSecret
clientId: appSettings.microsoftEntraID.clientId,
authority: `${appSettings.microsoftEntraID.instance}/${appSettings.microsoftEntraID.tenantId}`,
clientSecret: appSettings.microsoftEntraID.clientSecret
}
};
const confidentialClientApplication = new ConfidentialClientApplication(msalConfig);
Expand All @@ -34,18 +34,18 @@ export const createConfidentialClientApplication = (): ConfidentialClientApplica
*
* Notice: The incoming access token is generated by the client.
*/
export const exchangeAADTokenViaOBO = async (aadToken: string): Promise<string> => {
export const exchangeMEIDTokenViaOBO = async (aadToken: string): Promise<string> => {
const confidentialClientApplication = createConfidentialClientApplication();
// Exchange the incoming access token for another access token
try {
const oboRequest = {
oboAssertion: aadToken, // The access token that was sent to the middle-tier API. This token must have an audience of the app making this OBO request.
scopes: ['user.read'] // Array of scopes the application is requesting access to.
};
const aadTokenResponseViaOBO = await confidentialClientApplication.acquireTokenOnBehalfOf(oboRequest);
return aadTokenResponseViaOBO.accessToken;
const meidTokenResponseViaOBO = await confidentialClientApplication.acquireTokenOnBehalfOf(oboRequest);
return meidTokenResponseViaOBO.accessToken;
} catch (error) {
console.log(EXCHANGE_AAD_TOKEN_VIA_OBO_ERROR);
console.log(EXCHANGE_MEID_TOKEN_VIA_OBO_ERROR);
throw error;
}
};
12 changes: 6 additions & 6 deletions src/services/acsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const CREATE_ACS_TOKEN_ERROR = 'An error occurred when creating an ACS token';
const CREATE_ACS_USER_IDENTITY_TOKEN_ERROR =
'An error occurred when creating an ACS user id and issuing an access token for it in one go';
const DELETE_ACS_USER_IDENTITY_ERROR = 'An error occurred when deleting an ACS user id';
const EXCHANGE_AAD_TOKEN_ERROR = 'An error occurred when exchanging an AAD token';
const EXCHANGE_MEID_TOKEN_ERROR = 'An error occurred when exchanging an Microsoft Entra ID token';

/**
* Instantiate the identity client using the connection string.
Expand Down Expand Up @@ -65,24 +65,24 @@ export const createACSToken = async (acsUserId: string): Promise<CommunicationAc

/**
* Exchange an AAD access token of a Teams user for a new Communication Services AccessToken with a matching expiration time.
* @param teamsUserAadToken - The Azure AD token of the Teams user
* @param teamsUserMeidToken - The Azure AD token of the Teams user
* @param userObjectId - Object ID of an Azure AD user (Teams User) to be verified against the OID claim in the Azure AD access token.
*/
export const getACSTokenForTeamsUser = async (
teamsUserAadToken: string,
teamsUserMeidToken: string,
userObjectId: string
): Promise<CommunicationAccessToken> => {
const identityClient = createAuthenticatedClient();
try {
// Issue an access token for the Teams user that can be used with the Azure Communication Services SDKs.
const clientId = appSettings.azureActiveDirectory.clientId;
const clientId = appSettings.microsoftEntraID.clientId;
return await identityClient.getTokenForTeamsUser({
clientId: clientId,
teamsUserAadToken: teamsUserAadToken,
teamsUserAadToken: teamsUserMeidToken,
userObjectId: userObjectId
});
} catch (error) {
const errorMessage = `${EXCHANGE_AAD_TOKEN_ERROR}: ${error.message}`;
const errorMessage = `${EXCHANGE_MEID_TOKEN_ERROR}: ${error.message}`;
console.log(errorMessage);
throw new Error(errorMessage);
}
Expand Down
4 changes: 2 additions & 2 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { appSettings } from '../appSettings';
import jwtAuthz from 'express-jwt-authz';

// Get an AAD token passed through request header
export const getAADTokenViaRequest = (req: Request): string => {
export const getMEIDTokenViaRequest = (req: Request): string => {
return req.headers.authorization.split(' ')[1];
};

Expand All @@ -33,7 +33,7 @@ export const checkJwt = jwt.expressjwt({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://login.microsoftonline.com/${appSettings.azureActiveDirectory.tenantId}/discovery/keys?appid=${appSettings.azureActiveDirectory.clientId}` // Obtain public signing keys from a well-known URL
jwksUri: `https://login.microsoftonline.com/${appSettings.microsoftEntraID.tenantId}/discovery/keys?appid=${appSettings.microsoftEntraID.clientId}` // Obtain public signing keys from a well-known URL
}) as GetVerificationKey,
requestProperty: 'user', // Name of the property in the request object where the payload is set.
algorithms: ['RS256']
Expand Down
22 changes: 11 additions & 11 deletions tests/controllers/tokenController/exchangeAADToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,42 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../../../node_modules/@types/jest/index.d.ts" />

import { exchangeAADToken } from '../../../src/controllers/tokenController';
import { exchangeMEIDToken } from '../../../src/controllers/tokenController';
import {
mockCommunicationAccessToken,
mockAuthorization,
mockRequest,
mockResponse,
mockAuthenticatedRequest,
mockAadUserObjectId,
mockAadTokenWithDelegatedPermissions
mockMeidUserObjectId,
mockMeidTokenWithDelegatedPermissions
} from '../../utils/mockData';
import * as acsService from '../../../src/services/acsService';

let getACSTokenForTeamsUserSpy: jest.SpyInstance;

describe('Token Controller - Exchange AAD Token: ', () => {
describe('Token Controller - Exchange Microsoft Entra Token: ', () => {
test('when request has no authorization header, it should return an error.', async () => {
const req = mockRequest();
const res = mockResponse();

await exchangeAADToken(req, res, () => {
await exchangeMEIDToken(req, res, () => {
return res.status(500);
});

expect(res.status).toHaveBeenCalledWith(500);
});

test('when failing to get ACS Token for Teams User, it should return an error.', async () => {
const req = mockAuthenticatedRequest(mockAuthorization, mockAadUserObjectId, undefined, {
'teams-user-aad-token': mockAadTokenWithDelegatedPermissions
const req = mockAuthenticatedRequest(mockAuthorization, mockMeidUserObjectId, undefined, {
'teams-user-meid-token': mockMeidTokenWithDelegatedPermissions
});
const res = mockResponse();
getACSTokenForTeamsUserSpy = jest
.spyOn(acsService, 'getACSTokenForTeamsUser')
.mockImplementation(async () => new Promise((resolve, reject) => reject(undefined)));

await exchangeAADToken(req, res, () => {
await exchangeMEIDToken(req, res, () => {
return res.status(500);
});

Expand All @@ -51,15 +51,15 @@ describe('Token Controller - Exchange AAD Token: ', () => {
});

test('when successful to get ACS Token for Teams User, it should return a response with status 201 and an ACS token object.', async () => {
const req = mockAuthenticatedRequest(mockAuthorization, mockAadUserObjectId, undefined, {
'teams-user-aad-token': mockAadTokenWithDelegatedPermissions
const req = mockAuthenticatedRequest(mockAuthorization, mockMeidUserObjectId, undefined, {
'teams-user-aad-token': mockMeidTokenWithDelegatedPermissions
});
const res = mockResponse();
getACSTokenForTeamsUserSpy = jest
.spyOn(acsService, 'getACSTokenForTeamsUser')
.mockImplementation(async () => mockCommunicationAccessToken);

await exchangeAADToken(req, res, () => {
await exchangeMEIDToken(req, res, () => {
return res.status(500);
});

Expand Down
Loading