From 3ff2cb9aeccf61a8b784d5aa3a6d10f6b3bc5acf Mon Sep 17 00:00:00 2001 From: sameerag Date: Tue, 6 Jul 2021 23:42:39 -0700 Subject: [PATCH 1/7] ROPC added for confidential clients --- lib/msal-node/src/client/ClientApplication.ts | 39 ++++++++++++++++++- .../src/client/PublicClientApplication.ts | 37 +----------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/lib/msal-node/src/client/ClientApplication.ts b/lib/msal-node/src/client/ClientApplication.ts index 6e24f1a17a..548e8d06e0 100644 --- a/lib/msal-node/src/client/ClientApplication.ts +++ b/lib/msal-node/src/client/ClientApplication.ts @@ -19,6 +19,8 @@ import { CommonRefreshTokenRequest, CommonAuthorizationCodeRequest, CommonAuthorizationUrlRequest, + CommonUsernamePasswordRequest, + UsernamePasswordClient, AuthenticationScheme, ResponseMode, AuthorityOptions, @@ -36,6 +38,7 @@ import { AuthorizationCodeRequest } from "../request/AuthorizationCodeRequest"; import { RefreshTokenRequest } from "../request/RefreshTokenRequest"; import { SilentFlowRequest } from "../request/SilentFlowRequest"; import { version, name } from "../packageMetadata"; +import { UsernamePasswordRequest } from "../request/UsernamePasswordRequest"; /** * Base abstract class for all ClientApplications - public and confidential @@ -99,7 +102,7 @@ export abstract class ClientApplication { responseMode: request.responseMode || ResponseMode.QUERY, authenticationScheme: AuthenticationScheme.BEARER }; - + const authClientConfig = await this.buildOauthClientConfiguration( validRequest.authority, validRequest.correlationId @@ -210,6 +213,38 @@ export abstract class ClientApplication { } } + /** + * Acquires tokens with password grant by exchanging client applications username and password for credentials + * + * The latest OAuth 2.0 Security Best Current Practice disallows the password grant entirely. + * More details on this recommendation at https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-3.4 + * Microsoft's documentation and recommendations are at: + * https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#usernamepassword + * + * @param request - UsenamePasswordRequest + */ + async acquireTokenByUsernamePassword(request: UsernamePasswordRequest): Promise { + this.logger.info("acquireTokenByUsernamePassword called", request.correlationId); + const validRequest: CommonUsernamePasswordRequest = { + ...request, + ...this.initializeBaseRequest(request) + }; + const serverTelemetryManager = this.initializeServerTelemetryManager(ApiId.acquireTokenByUsernamePassword, validRequest.correlationId!); + try { + const usernamePasswordClientConfig = await this.buildOauthClientConfiguration( + validRequest.authority, + validRequest.correlationId, + serverTelemetryManager + ); + const usernamePasswordClient = new UsernamePasswordClient(usernamePasswordClientConfig); + this.logger.verbose("Username password client created", validRequest.correlationId); + return usernamePasswordClient.acquireToken(validRequest); + } catch (e) { + serverTelemetryManager.cacheFailedRequest(e); + throw e; + } + } + /** * Gets the token cache for the application. */ @@ -335,7 +370,7 @@ export abstract class ClientApplication { knownAuthorities: this.config.auth.knownAuthorities!, cloudDiscoveryMetadata: this.config.auth.cloudDiscoveryMetadata!, authorityMetadata: this.config.auth.authorityMetadata!, - azureRegionConfiguration + azureRegionConfiguration }; return await AuthorityFactory.createDiscoveredInstance(authorityString, this.config.system!.networkClient!, this.storage, authorityOptions); } diff --git a/lib/msal-node/src/client/PublicClientApplication.ts b/lib/msal-node/src/client/PublicClientApplication.ts index 12aa7c6db2..78b28f93e2 100644 --- a/lib/msal-node/src/client/PublicClientApplication.ts +++ b/lib/msal-node/src/client/PublicClientApplication.ts @@ -7,15 +7,12 @@ import { ApiId } from "../utils/Constants"; import { DeviceCodeClient, AuthenticationResult, - CommonDeviceCodeRequest, - CommonUsernamePasswordRequest, - UsernamePasswordClient + CommonDeviceCodeRequest } from "@azure/msal-common"; import { Configuration } from "../config/Configuration"; import { ClientApplication } from "./ClientApplication"; import { IPublicClientApplication } from "./IPublicClientApplication"; import { DeviceCodeRequest } from "../request/DeviceCodeRequest"; -import { UsernamePasswordRequest } from "../request/UsernamePasswordRequest"; /** * This class is to be used to acquire tokens for public client applications (desktop, mobile). Public client applications @@ -71,36 +68,4 @@ export class PublicClientApplication extends ClientApplication implements IPubli throw e; } } - - /** - * Acquires tokens with password grant by exchanging client applications username and password for credentials - * - * The latest OAuth 2.0 Security Best Current Practice disallows the password grant entirely. - * More details on this recommendation at https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-3.4 - * Microsoft's documentation and recommendations are at: - * https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#usernamepassword - * - * @param request - UsenamePasswordRequest - */ - async acquireTokenByUsernamePassword(request: UsernamePasswordRequest): Promise { - this.logger.info("acquireTokenByUsernamePassword called", request.correlationId); - const validRequest: CommonUsernamePasswordRequest = { - ...request, - ...this.initializeBaseRequest(request) - }; - const serverTelemetryManager = this.initializeServerTelemetryManager(ApiId.acquireTokenByUsernamePassword, validRequest.correlationId!); - try { - const usernamePasswordClientConfig = await this.buildOauthClientConfiguration( - validRequest.authority, - validRequest.correlationId, - serverTelemetryManager - ); - const usernamePasswordClient = new UsernamePasswordClient(usernamePasswordClientConfig); - this.logger.verbose("Username password client created", validRequest.correlationId); - return usernamePasswordClient.acquireToken(validRequest); - } catch (e) { - serverTelemetryManager.cacheFailedRequest(e); - throw e; - } - } } From a58c09566586b6e088c5ed480460298a459015a7 Mon Sep 17 00:00:00 2001 From: sameerag Date: Wed, 7 Jul 2021 00:01:17 -0700 Subject: [PATCH 2/7] Added tests --- .../ConfidentialClientApplication.spec.ts | 28 +++++++++++++++---- .../client/PublicClientApplication.spec.ts | 19 +++++++++++++ lib/msal-node/test/utils/TestConstants.ts | 4 ++- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts b/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts index 6282897d5d..90fe1456c1 100644 --- a/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts +++ b/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts @@ -1,5 +1,5 @@ import { ConfidentialClientApplication } from './../../src/client/ConfidentialClientApplication'; -import { Authority, ClientConfiguration, AuthorityFactory, AuthorizationCodeClient, RefreshTokenClient, StringUtils } from '@azure/msal-common'; +import { Authority, ClientConfiguration, AuthorityFactory, AuthorizationCodeClient, RefreshTokenClient, StringUtils, ClientCredentialClient, OnBehalfOfClient, UsernamePasswordClient } from '@azure/msal-common'; import { TEST_CONSTANTS } from '../utils/TestConstants'; import { Configuration } from "../../src/config/Configuration"; import { AuthorizationCodeRequest } from "../../src/request/AuthorizationCodeRequest"; @@ -7,6 +7,7 @@ import { mocked } from 'ts-jest/utils'; import { RefreshTokenRequest } from "../../src/request/RefreshTokenRequest"; import { ClientCredentialRequest } from "../../src/request/ClientCredentialRequest"; import { OnBehalfOfRequest } from "../../src/request/OnBehalfOfRequest"; +import { UsernamePasswordRequest } from '../../src/request/UsernamePasswordRequest'; jest.mock('@azure/msal-common'); @@ -93,8 +94,8 @@ describe('ConfidentialClientApplication', () => { const authApp = new ConfidentialClientApplication(appConfig); await authApp.acquireTokenByClientCredential(request); - expect(AuthorizationCodeClient).toHaveBeenCalledTimes(1); - expect(AuthorizationCodeClient).toHaveBeenCalledWith( + expect(ClientCredentialClient).toHaveBeenCalledTimes(1); + expect(ClientCredentialClient).toHaveBeenCalledWith( expect.objectContaining(expectedConfig) ); }); @@ -109,8 +110,25 @@ describe('ConfidentialClientApplication', () => { const authApp = new ConfidentialClientApplication(appConfig); await authApp.acquireTokenOnBehalfOf(request); - expect(AuthorizationCodeClient).toHaveBeenCalledTimes(1); - expect(AuthorizationCodeClient).toHaveBeenCalledWith( + expect(OnBehalfOfClient).toHaveBeenCalledTimes(1); + expect(OnBehalfOfClient).toHaveBeenCalledWith( + expect.objectContaining(expectedConfig) + ); + }); + + test('acquireTokenByUsernamePassword', async () => { + const request: UsernamePasswordRequest = { + scopes: TEST_CONSTANTS.DEFAULT_GRAPH_SCOPE, + username: TEST_CONSTANTS.USERNAME, + password: TEST_CONSTANTS.PASSWORD + }; + + mocked(AuthorityFactory.createDiscoveredInstance).mockReturnValue(Promise.resolve(authority)); + + const authApp = new ConfidentialClientApplication(appConfig); + await authApp.acquireTokenByUsernamePassword(request); + expect(UsernamePasswordClient).toHaveBeenCalledTimes(1); + expect(UsernamePasswordClient).toHaveBeenCalledWith( expect.objectContaining(expectedConfig) ); }); diff --git a/lib/msal-node/test/client/PublicClientApplication.spec.ts b/lib/msal-node/test/client/PublicClientApplication.spec.ts index 9826b07e27..46840c7fef 100644 --- a/lib/msal-node/test/client/PublicClientApplication.spec.ts +++ b/lib/msal-node/test/client/PublicClientApplication.spec.ts @@ -9,6 +9,7 @@ import { AuthorizationCodeClient, DeviceCodeClient, RefreshTokenClient, + UsernamePasswordClient, ClientConfiguration, ProtocolMode, Logger, @@ -18,6 +19,7 @@ import { import { AuthorizationUrlRequest } from "../../src/request/AuthorizationUrlRequest"; import { DeviceCodeRequest } from "../../src/request/DeviceCodeRequest"; import { RefreshTokenRequest } from "../../src/request/RefreshTokenRequest"; +import { UsernamePasswordRequest } from '../../src/request/UsernamePasswordRequest'; import { NodeStorage } from "../../src/cache/NodeStorage"; import { HttpClient } from "../../src/network/HttpClient"; @@ -130,6 +132,23 @@ describe('PublicClientApplication', () => { ); }); + test('acquireTokenByUsernamePassword', async () => { + const request: UsernamePasswordRequest = { + scopes: TEST_CONSTANTS.DEFAULT_GRAPH_SCOPE, + username: TEST_CONSTANTS.USERNAME, + password: TEST_CONSTANTS.PASSWORD + }; + + mocked(AuthorityFactory.createDiscoveredInstance).mockReturnValue(Promise.resolve(authority)); + + const authApp = new PublicClientApplication(appConfig); + await authApp.acquireTokenByUsernamePassword(request); + expect(UsernamePasswordClient).toHaveBeenCalledTimes(1); + expect(UsernamePasswordClient).toHaveBeenCalledWith( + expect.objectContaining(expectedConfig) + ); + }); + test('acquireToken default authority', async () => { // No authority set in app configuration or request, should default to common authority const config: Configuration = { diff --git a/lib/msal-node/test/utils/TestConstants.ts b/lib/msal-node/test/utils/TestConstants.ts index a09f1ffe88..d7f205df0c 100644 --- a/lib/msal-node/test/utils/TestConstants.ts +++ b/lib/msal-node/test/utils/TestConstants.ts @@ -14,6 +14,8 @@ export const TEST_CONSTANTS = { REDIRECT_URI: "http://localhost:8080", CLIENT_SECRET: "MOCK_CLIENT_SECRET", DEFAULT_GRAPH_SCOPE: ["user.read"], + USERNAME: "mockuser", + PASSWORD: "mockpassword", AUTHORIZATION_CODE: "0.ASgAqPq4kJXMDkamGO53C-4XWVm3ypmrKgtCkdhePY1PBjsoAJg.AQABAAIAAAAm-06blBE1TpVMil8KPQ41DOje1jDj1oK3KxTXGKg89VjLYJi71gx_npOoxVfC7X49MqOX7IltTJOilUId-IAHndHXlfWzoSGq3GUmwAOLMisftceBRtq3YBsvHX7giiuSZXJgpgu03uf3V2h5Z3GJNpnSXT1f7iVFuRvGh1-jqjWxKs2un8AS5rhti1ym1zxkeicKT43va5jQeHVUlTQo69llnwQJ3iKmKLDVq_Q25Au4EQjYaeEx6TP5IZSqPPm7x0bynmjE8cqR5r4ySP4wH8fjnxlLySrUEZObk2VgREB1AdH6-xKIa04EnJEj9dUgTwiFvQumkuHHetFOgH7ep_9diFOdAOQLUK8C9N4Prlj0JiOcgn6l0xYd5Q9691Ylw8UfifLwq_B7f30mMLN64_XgoBY9K9CR1L4EC1kPPwIhVv3m6xmbhXZ3efx-A-bbV2SYcO4D4ZlnQztHzie_GUlredtsdEMAOE3-jaMJs7i2yYMuIEEtRcHIjV_WscVooCDdKmVncHOObWhNUSdULAejBr3pFs0v3QO_xZ269eLu5Z0qHzCZ_EPg2aL-ERz-rpgdclQ_H_KnEtMsC4F1RgAnDjVmSRKJZZdnNLfKSX_Wd40t_nuo4kjN2cSt8QzzeL533zIZ4CxthOsC4HH2RcUZDIgHdLDLT2ukg-Osc6J9URpZP-IUpdjXg_uwbkHEjrXDMBMo2pmCqaWbMJKo5Lr7CrystifnDITXzZmmOah8HV83Xyb6EP8Gno6JRuaG80j8BKDWyb1Yof4rnLI1kZ59n_t2d0LnRBXz50PdWCWX6vtkg-kAV-bGJQr45XDSKBSv0Q_fVsdLMk24NacUZcF5ujUtqv__Bv-wATzCHWlbUDGHC8nHEi84PcYAjSsgAA", ACCESS_TOKEN: "ThisIsAnAccessT0ken", @@ -24,7 +26,7 @@ export const TEST_CONSTANTS = { CLIENT_ASSERTION: "MOCK_CLIENT_ASSERTION", THUMBPRINT: "6182de7d4b84517655fe0bfa97076890d66bf37a", PRIVATE_KEY: "PRIVATE_KEY", - PUBLIC_CERTIFICATE: + PUBLIC_CERTIFICATE: `-----BEGIN CERTIFICATE----- line1 line2 From efb39ddd8be4fc9d4b93b36306cd782a8b27ae32 Mon Sep 17 00:00:00 2001 From: sameerag Date: Wed, 7 Jul 2021 00:03:24 -0700 Subject: [PATCH 3/7] Change files --- ...ure-msal-node-c1284212-4af4-45ff-829d-e7c277947e88.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@azure-msal-node-c1284212-4af4-45ff-829d-e7c277947e88.json diff --git a/change/@azure-msal-node-c1284212-4af4-45ff-829d-e7c277947e88.json b/change/@azure-msal-node-c1284212-4af4-45ff-829d-e7c277947e88.json new file mode 100644 index 0000000000..aca5cae53a --- /dev/null +++ b/change/@azure-msal-node-c1284212-4af4-45ff-829d-e7c277947e88.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "ROPC added for Confidential Clients (#3838)", + "packageName": "@azure/msal-node", + "email": "sameera.gajjarapu@microsoft.com", + "dependentChangeType": "patch" +} From be3f7bd2ae704cfa2d0f641e6061cf59543cb618 Mon Sep 17 00:00:00 2001 From: sameerag Date: Fri, 9 Jul 2021 14:13:51 -0700 Subject: [PATCH 4/7] Add public API in ConfidentialClient interface --- lib/msal-node/src/client/IConfidentialClientApplication.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/msal-node/src/client/IConfidentialClientApplication.ts b/lib/msal-node/src/client/IConfidentialClientApplication.ts index a99810656b..db684282fb 100644 --- a/lib/msal-node/src/client/IConfidentialClientApplication.ts +++ b/lib/msal-node/src/client/IConfidentialClientApplication.ts @@ -10,6 +10,7 @@ import { ClientCredentialRequest } from "../request/ClientCredentialRequest"; import { OnBehalfOfRequest } from "../request/OnBehalfOfRequest"; import { RefreshTokenRequest } from "../request/RefreshTokenRequest"; import { SilentFlowRequest } from "../request/SilentFlowRequest"; +import { UsernamePasswordRequest } from "../request/UsernamePasswordRequest"; import { TokenCache } from "../cache/TokenCache"; /** @@ -36,6 +37,9 @@ export interface IConfidentialClientApplication { /** Acquires tokens from the authority for the application */ acquireTokenOnBehalfOf(request: OnBehalfOfRequest): Promise; + /** Acquires tokens with password grant by exchanging client applications username and password for credentials */ + acquireTokenByUsernamePassword(request: UsernamePasswordRequest): Promise; + /** Gets the token cache for the application */ getTokenCache(): TokenCache; From 835e740642402519fc1f61fe30b24a47d8fceb5c Mon Sep 17 00:00:00 2001 From: sameerag Date: Fri, 9 Jul 2021 15:43:42 -0700 Subject: [PATCH 5/7] Adding Samples --- .../src/client/UsernamePasswordClient.ts | 6 +++- .../username-password-cca/.npmrc | 1 + .../username-password-cca/index.js | 31 +++++++++++++++++++ .../username-password-cca/package.json | 18 +++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 samples/msal-node-samples/username-password-cca/.npmrc create mode 100644 samples/msal-node-samples/username-password-cca/index.js create mode 100644 samples/msal-node-samples/username-password-cca/package.json diff --git a/lib/msal-common/src/client/UsernamePasswordClient.ts b/lib/msal-common/src/client/UsernamePasswordClient.ts index 4818f4297f..e62396dca1 100644 --- a/lib/msal-common/src/client/UsernamePasswordClient.ts +++ b/lib/msal-common/src/client/UsernamePasswordClient.ts @@ -94,7 +94,7 @@ export class UsernamePasswordClient extends BaseClient { parameterBuilder.addLibraryInfo(this.config.libraryInfo); parameterBuilder.addThrottling(); - + if (this.serverTelemetryManager) { parameterBuilder.addServerTelemetry(this.serverTelemetryManager); } @@ -102,6 +102,10 @@ export class UsernamePasswordClient extends BaseClient { const correlationId = request.correlationId || this.config.cryptoInterface.createNewGuid(); parameterBuilder.addCorrelationId(correlationId); + if (this.config.clientCredentials.clientSecret) { + parameterBuilder.addClientSecret(this.config.clientCredentials.clientSecret); + } + if (!StringUtils.isEmptyObj(request.claims) || this.config.authOptions.clientCapabilities && this.config.authOptions.clientCapabilities.length > 0) { parameterBuilder.addClaims(request.claims, this.config.authOptions.clientCapabilities); } diff --git a/samples/msal-node-samples/username-password-cca/.npmrc b/samples/msal-node-samples/username-password-cca/.npmrc new file mode 100644 index 0000000000..43c97e719a --- /dev/null +++ b/samples/msal-node-samples/username-password-cca/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/samples/msal-node-samples/username-password-cca/index.js b/samples/msal-node-samples/username-password-cca/index.js new file mode 100644 index 0000000000..aa5a933ade --- /dev/null +++ b/samples/msal-node-samples/username-password-cca/index.js @@ -0,0 +1,31 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +var msal = require("@azure/msal-node"); + +const msalConfig = { + auth: { + clientId: "", // Enter your client_id here + authority: "", // Enter your authority here + clientSecret: "" // Enter your client_secret here + } +}; + +const cca = new msal.ConfidentialClientApplication(msalConfig); + +const usernamePasswordRequest = { + scopes: ["user.read"], + username: "", // Add your username here + password: "", // Add your password here +}; + +cca.acquireTokenByUsernamePassword(usernamePasswordRequest).then((response) => { + console.log("acquired token by password grant in confidential clients"); +}).catch((error) => { + console.log(error); +}); + + + diff --git a/samples/msal-node-samples/username-password-cca/package.json b/samples/msal-node-samples/username-password-cca/package.json new file mode 100644 index 0000000000..47d26349db --- /dev/null +++ b/samples/msal-node-samples/username-password-cca/package.json @@ -0,0 +1,18 @@ +{ + "name": "msal-node-username-password-cca", + "version": "1.0.0", + "description": "Command line app that uses Oauth password grant flow to get a token from Azure AD", + "main": "index.js", + "private": true, + "scripts": { + "start": "node index.js", + "build:package": "cd ../../lib/msal-common && npm run build && cd ../msal-node && npm run build", + "start:build": "npm run build:package && npm start", + "install:local": "npm install ../../../lib/msal-node" + }, + "author": "Microsoft", + "license": "MIT", + "dependencies": { + "@azure/msal-node": "file:../../../lib/msal-node" + } +} From c467d18adff33f98071837ee0fd51d4259236b9f Mon Sep 17 00:00:00 2001 From: sameerag Date: Fri, 9 Jul 2021 15:45:46 -0700 Subject: [PATCH 6/7] Change files --- ...e-msal-common-dc2c24bd-f38c-4580-ba04-b44ee8ac5b9f.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@azure-msal-common-dc2c24bd-f38c-4580-ba04-b44ee8ac5b9f.json diff --git a/change/@azure-msal-common-dc2c24bd-f38c-4580-ba04-b44ee8ac5b9f.json b/change/@azure-msal-common-dc2c24bd-f38c-4580-ba04-b44ee8ac5b9f.json new file mode 100644 index 0000000000..ad91dc7ee7 --- /dev/null +++ b/change/@azure-msal-common-dc2c24bd-f38c-4580-ba04-b44ee8ac5b9f.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Adding ROPC for confidential client apps (#3838)", + "packageName": "@azure/msal-common", + "email": "sameera.gajjarapu@microsoft.com", + "dependentChangeType": "patch" +} From 7f2e29fb115893144ac83d00e890b5f0ee65861a Mon Sep 17 00:00:00 2001 From: sameerag Date: Mon, 12 Jul 2021 14:15:54 -0700 Subject: [PATCH 7/7] Add client assertion --- lib/msal-common/src/client/UsernamePasswordClient.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/msal-common/src/client/UsernamePasswordClient.ts b/lib/msal-common/src/client/UsernamePasswordClient.ts index e62396dca1..e94a6d8d3c 100644 --- a/lib/msal-common/src/client/UsernamePasswordClient.ts +++ b/lib/msal-common/src/client/UsernamePasswordClient.ts @@ -106,6 +106,12 @@ export class UsernamePasswordClient extends BaseClient { parameterBuilder.addClientSecret(this.config.clientCredentials.clientSecret); } + if (this.config.clientCredentials.clientAssertion) { + const clientAssertion = this.config.clientCredentials.clientAssertion; + parameterBuilder.addClientAssertion(clientAssertion.assertion); + parameterBuilder.addClientAssertionType(clientAssertion.assertionType); + } + if (!StringUtils.isEmptyObj(request.claims) || this.config.authOptions.clientCapabilities && this.config.authOptions.clientCapabilities.length > 0) { parameterBuilder.addClaims(request.claims, this.config.authOptions.clientCapabilities); }