Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Communication]: Add SDK operation to exchange access token #15449

Merged
26 commits merged into from
May 28, 2021
Merged
Show file tree
Hide file tree
Changes from 20 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
3 changes: 2 additions & 1 deletion sdk/communication/communication-identity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,13 @@
"@azure/core-http": "^1.2.0",
"@azure/core-lro": "^1.0.2",
"@azure/core-paging": "^1.1.1",
"@azure/logger": "^1.0.0",
"@azure/core-tracing": "1.0.0-preview.11",
"@azure/logger": "^1.0.0",
"events": "^3.0.0",
"tslib": "^2.0.0"
},
"devDependencies": {
"@azure/msal-node": "^1.0.2",
"@azure/dev-tool": "^1.0.0",
"@azure/eslint-plugin-azure-sdk": "^3.0.0",
"@azure/test-utils": "^1.0.0",
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class CommunicationIdentityClient {
createUser(options?: OperationOptions): Promise<CommunicationUserIdentifier>;
createUserAndToken(scopes: TokenScope[], options?: OperationOptions): Promise<CommunicationUserToken>;
deleteUser(user: CommunicationUserIdentifier, options?: OperationOptions): Promise<void>;
exchangeAADtokenForACStoken(token: string, options?: OperationOptions): Promise<CommunicationAccessToken>;
thdinizm marked this conversation as resolved.
Show resolved Hide resolved
getToken(user: CommunicationUserIdentifier, scopes: TokenScope[], options?: OperationOptions): Promise<CommunicationAccessToken>;
revokeTokens(user: CommunicationUserIdentifier, options?: OperationOptions): Promise<void>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,35 @@ export class CommunicationIdentityClient {
span.end();
}
}

/**
* Exchanges an AAD access token of a Teams user for a new ACS access token.
*
* @param token - The AAD access token.
* @param options - Additional options for the request.
*/
public async exchangeAADtokenForACStoken(
token: string,
options: OperationOptions = {}
): Promise<CommunicationAccessToken> {
const { span, updatedOptions } = createSpan(
"CommunicationIdentity-exchangeAADtokenForACStoken",
options
);
try {
const { _response, ...result } = await this.client.exchangeTeamsUserAccessToken(
{ token },
operationOptionsToRequestOptionsBase(updatedOptions)
);
return result;
} catch (e) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: e.message
});
throw e;
} finally {
span.end();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@ export class IdentityRestClientContext extends coreHttp.ServiceClient {
this.endpoint = endpoint;

// Assigning values to Constant parameters
this.apiVersion = options.apiVersion || "2021-03-07";
this.apiVersion = options.apiVersion || "2021-03-31-preview1";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ export interface CommunicationError {
readonly innerError?: CommunicationError;
}

export interface TeamsUserAccessTokenRequest {
/**
* Azure Active Directory access token to acquire a new ACS access token.
*/
token: string;
}

export interface CommunicationIdentityAccessTokenRequest {
/**
* List of scopes attached to the token.
Expand Down Expand Up @@ -129,6 +136,26 @@ export type CommunicationIdentityCreateResponse = CommunicationIdentityAccessTok
};
};

/**
* Contains response data for the exchangeTeamsUserAccessToken operation.
*/
export type CommunicationIdentityExchangeTeamsUserAccessTokenResponse = CommunicationIdentityAccessToken & {
/**
* The underlying HTTP response.
*/
_response: coreHttp.HttpResponse & {
/**
* The response body as text (string format)
*/
bodyAsText: string;

/**
* The response body as parsed JSON or XML
*/
parsedBody: CommunicationIdentityAccessToken;
};
};

/**
* Contains response data for the issueAccessToken operation.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,22 @@ export const CommunicationError: coreHttp.CompositeMapper = {
}
};

export const TeamsUserAccessTokenRequest: coreHttp.CompositeMapper = {
type: {
name: "Composite",
className: "TeamsUserAccessTokenRequest",
modelProperties: {
token: {
serializedName: "token",
required: true,
type: {
name: "String"
}
}
}
}
};

export const CommunicationIdentityAccessTokenRequest: coreHttp.CompositeMapper = {
type: {
name: "Composite",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "@azure/core-http";
import {
CommunicationIdentityCreateRequest as CommunicationIdentityCreateRequestMapper,
TeamsUserAccessTokenRequest as TeamsUserAccessTokenRequestMapper,
CommunicationIdentityAccessTokenRequest as CommunicationIdentityAccessTokenRequestMapper
} from "../models/mappers";

Expand Down Expand Up @@ -48,7 +49,7 @@ export const endpoint: OperationURLParameter = {
export const apiVersion: OperationQueryParameter = {
parameterPath: "apiVersion",
mapper: {
defaultValue: "2021-03-07",
defaultValue: "2021-03-31-preview1",
isConstant: true,
serializedName: "api-version",
type: {
Expand All @@ -69,6 +70,11 @@ export const id: OperationURLParameter = {
};

export const body1: OperationParameter = {
parameterPath: "body",
mapper: TeamsUserAccessTokenRequestMapper
};

export const body2: OperationParameter = {
parameterPath: "body",
mapper: CommunicationIdentityAccessTokenRequestMapper
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { IdentityRestClient } from "../identityRestClient";
import {
CommunicationIdentityCreateOptionalParams,
CommunicationIdentityCreateResponse,
TeamsUserAccessTokenRequest,
CommunicationIdentityExchangeTeamsUserAccessTokenResponse,
CommunicationIdentityAccessTokenRequest,
CommunicationIdentityIssueAccessTokenResponse
} from "../models";
Expand Down Expand Up @@ -83,6 +85,24 @@ export class CommunicationIdentity {
) as Promise<coreHttp.RestResponse>;
}

/**
* Exchange an AAD access token of a Teams user for a new ACS access token.
* @param body
* @param options The options parameters.
*/
exchangeTeamsUserAccessToken(
body: TeamsUserAccessTokenRequest,
options?: coreHttp.OperationOptions
): Promise<CommunicationIdentityExchangeTeamsUserAccessTokenResponse> {
const operationOptions: coreHttp.RequestOptionsBase = coreHttp.operationOptionsToRequestOptionsBase(
options || {}
);
return this.client.sendOperationRequest(
{ body, options: operationOptions },
exchangeTeamsUserAccessTokenOperationSpec
) as Promise<CommunicationIdentityExchangeTeamsUserAccessTokenResponse>;
}

/**
* Issue a new token for an identity.
* @param id Identifier of the identity to issue token for.
Expand Down Expand Up @@ -151,6 +171,24 @@ const revokeAccessTokensOperationSpec: coreHttp.OperationSpec = {
urlParameters: [Parameters.endpoint, Parameters.id],
serializer
};
const exchangeTeamsUserAccessTokenOperationSpec: coreHttp.OperationSpec = {
path: "/teamsUser/:exchangeAccessToken",
httpMethod: "POST",
responses: {
200: {
bodyMapper: Mappers.CommunicationIdentityAccessToken
},
default: {
bodyMapper: Mappers.CommunicationErrorResponse
}
},
requestBody: Parameters.body1,
queryParameters: [Parameters.apiVersion],
urlParameters: [Parameters.endpoint],
headerParameters: [Parameters.contentType],
mediaType: "json",
serializer
};
const issueAccessTokenOperationSpec: coreHttp.OperationSpec = {
path: "/identities/{id}/:issueAccessToken",
httpMethod: "POST",
Expand All @@ -162,7 +200,7 @@ const issueAccessTokenOperationSpec: coreHttp.OperationSpec = {
bodyMapper: Mappers.CommunicationErrorResponse
}
},
requestBody: Parameters.body1,
requestBody: Parameters.body2,
queryParameters: [Parameters.apiVersion],
urlParameters: [Parameters.endpoint, Parameters.id],
headerParameters: [Parameters.contentType],
Expand Down
4 changes: 2 additions & 2 deletions sdk/communication/communication-identity/swagger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ package-version: 1.0.1
generate-metadata: false
license-header: MICROSOFT_MIT_NO_VERSION
output-folder: ../src/generated
tag: package-2021-03-07
require: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/896d05e37dbb00712726620b8d679cc3c3be09fb/specification/communication/data-plane/Identity/readme.md
tag: package-2021-03-31-preview1
require: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/communication/data-plane/Identity/readme.md
model-date-time-as-string: false
optional-response-headers: true
payload-flattening-threshold: 10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { assert } from "chai";
import sinon from "sinon";
import { CommunicationIdentityClient } from "../../src";
import { TestCommunicationIdentityClient } from "./utils/testCommunicationIdentityClient";
import { getTokenHttpClient } from "./utils/mockHttpClients";
import { exchangeAADtokenForACSTokenHttpClient, getTokenHttpClient } from "./utils/mockHttpClients";

describe("CommunicationIdentityClient [Mocked]", () => {
const dateHeader = isNode ? "date" : "x-ms-date";
Expand Down Expand Up @@ -76,4 +76,21 @@ describe("CommunicationIdentityClient [Mocked]", () => {
assert.equal(newUser.communicationUserId, "identity");
assert.isFalse("_response" in newUser);
});

it("exchanges AAD token for ACS token", async () => {
const client = new TestCommunicationIdentityClient();
const spy = sinon.spy(exchangeAADtokenForACSTokenHttpClient, "sendRequest");
const response = await client.exchangeAADtokenForACStokenTest("AADtoken");

assert.equal(response.token, "token");
assert.equal(response.expiresOn.toDateString(), new Date("2011/11/30").toDateString());
sinon.assert.calledOnce(spy);
});

it("[exchangeAADtokenForACSToken] excludes _response from results", async () => {
const client = new TestCommunicationIdentityClient();
const response = await client.exchangeAADtokenForACStokenTest("AADtoken");

assert.isFalse("_response" in response);
beltr0n marked this conversation as resolved.
Show resolved Hide resolved
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { assert } from "chai";
import { matrix } from "@azure/test-utils";
import { env, isPlaybackMode, Recorder } from "@azure/test-utils-recorder";
import * as msal from "@azure/msal-node";
import { CommunicationAccessToken, CommunicationIdentityClient } from "../../../src";
import {
createRecordedCommunicationIdentityClient,
createRecordedCommunicationIdentityClientWithToken
} from "../utils/recordedClient";
import { Context } from "mocha";

matrix([[true, false]], async function(useAad) {
describe(`Exchange AAD token for ACS token [Playback/Live]${useAad ? " [AAD]" : ""}`, function() {
let recorder: Recorder;
let client: CommunicationIdentityClient;

before(function(this: Context) {
const skipTests = env.SKIP_INT_IDENTITY_EXCHANGE_TOKEN_TEST === "true";
if (skipTests) {
this.skip();
} else if (isPlaybackMode()) {
this.skip();
}
});

beforeEach(async function(this: Context) {
if (useAad) {
({ client, recorder } = createRecordedCommunicationIdentityClientWithToken(this));
} else {
({ client, recorder } = createRecordedCommunicationIdentityClient(this));
}

await recorder.stop();
});

afterEach(async function(this: Context) {
if (!this.currentTest?.isPending()) {
await recorder.stop();
}
});

it("successfully exchanges an AAD token for an ACS token", async function() {
recorder.skip();

const msalConfig = {
auth: {
clientId: env.COMMUNICATION_M365_APP_ID,
authority: `${env.COMMUNICATION_M365_AAD_AUTHORITY}/${env.COMMUNICATION_M365_AAD_TENANT}`
}
};

const request = {
username: env.COMMUNICATION_MSAL_USERNAME,
password: env.COMMUNICATION_MSAL_PASSWORD,
scopes: [env.COMMUNICATION_M365_SCOPE]
};

const pca = new msal.PublicClientApplication(msalConfig);

const response = await pca.acquireTokenByUsernamePassword(request);
assert.isNotNull(response);

const {
token,
expiresOn
}: CommunicationAccessToken = await client.exchangeAADtokenForACStoken(response!.accessToken);
assert.isString(token);
assert.instanceOf(expiresOn, Date);
}).timeout(5000);

it("throws an error when attempting to exchange an invalid AAD token", async function() {
recorder.skip();

try {
await client.exchangeAADtokenForACStoken("invalid");
assert.fail("Should have thrown an error");
thdinizm marked this conversation as resolved.
Show resolved Hide resolved
} catch (e) {
assert.equal(e.statusCode, 401);
thdinizm marked this conversation as resolved.
Show resolved Hide resolved
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,7 @@ export const createUserAndTokenHttpClient: HttpClient = createMockHttpClient<
expiresOn: new Date("2011/11/30")
}
});

export const exchangeAADtokenForACSTokenHttpClient: HttpClient = createMockHttpClient<
CommunicationAccessToken
>(200, tokenResponse);
Loading