diff --git a/lib/msal-browser/test/utils/StringConstants.ts b/lib/msal-browser/test/utils/StringConstants.ts index 2ca8e38bb1..3810532cd7 100644 --- a/lib/msal-browser/test/utils/StringConstants.ts +++ b/lib/msal-browser/test/utils/StringConstants.ts @@ -243,7 +243,7 @@ export const ALTERNATE_OPENID_CONFIG_RESPONSE = { } }; -export const testNavUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${encodeURIComponent(`${TEST_CONFIG.MSAL_CLIENT_ID}`)}&scope=user.read%20openid%20profile%20offline_access&redirect_uri=https%3A%2F%2Flocalhost%3A8081%2Findex.html&client-request-id=${encodeURIComponent(`${RANDOM_TEST_GUID}`)}&response_mode=fragment&response_type=code&x-client-SKU=msal.js.browser&x-client-VER=${version}&x-client-OS=&x-client-CPU=&client_info=1&code_challenge=JsjesZmxJwehdhNY9kvyr0QOeSMEvryY_EHZo3BKrqg&code_challenge_method=S256&nonce=${encodeURIComponent(`${RANDOM_TEST_GUID}`)}&state=${encodeURIComponent(`${TEST_STATE_VALUES.TEST_STATE_REDIRECT}`)}`; +export const testNavUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${encodeURIComponent(`${TEST_CONFIG.MSAL_CLIENT_ID}`)}&scope=user.read%20openid%20profile%20offline_access&redirect_uri=https%3A%2F%2Flocalhost%3A8081%2Findex.html&client-request-id=${encodeURIComponent(`${RANDOM_TEST_GUID}`)}&response_mode=fragment&response_type=code&x-client-SKU=msal.js.browser&x-client-VER=${version}&x-client-OS=&x-client-CPU=&client_info=1&code_challenge=JsjesZmxJwehdhNY9kvyr0QOeSMEvryY_EHZo3BKrqg&code_challenge_method=S256&nonce=${encodeURIComponent(`${RANDOM_TEST_GUID}`)}&state=${encodeURIComponent(`${TEST_STATE_VALUES.TEST_STATE_REDIRECT}`)}&stk_jwk=${TEST_POP_VALUES.ENCODED_STK_JWK_THUMBPRINT}`; export const testNavUrlNoRequest = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${encodeURIComponent(`${TEST_CONFIG.MSAL_CLIENT_ID}`)}&scope=openid%20profile%20offline_access&redirect_uri=https%3A%2F%2Flocalhost%3A8081%2Findex.html&client-request-id=${encodeURIComponent(`${RANDOM_TEST_GUID}`)}&response_mode=fragment&response_type=code&x-client-SKU=msal.js.browser&x-client-VER=${version}&x-client-OS=&x-client-CPU=&client_info=1&code_challenge=JsjesZmxJwehdhNY9kvyr0QOeSMEvryY_EHZo3BKrqg&code_challenge_method=S256&nonce=${encodeURIComponent(`${RANDOM_TEST_GUID}`)}&state=`; diff --git a/lib/msal-common/src/client/AuthorizationCodeClient.ts b/lib/msal-common/src/client/AuthorizationCodeClient.ts index 6c347bf730..21f73b788d 100644 --- a/lib/msal-common/src/client/AuthorizationCodeClient.ts +++ b/lib/msal-common/src/client/AuthorizationCodeClient.ts @@ -245,6 +245,11 @@ export class AuthorizationCodeClient extends BaseClient { } } + if (request.stkJwk) { + const stkJwk = await this.popTokenGenerator.retrieveAsymmetricPublicKey(request.stkJwk); + parameterBuilder.addStkJwk(stkJwk); + } + const correlationId = request.correlationId || this.config.cryptoInterface.createNewGuid(); parameterBuilder.addCorrelationId(correlationId); @@ -401,6 +406,10 @@ export class AuthorizationCodeClient extends BaseClient { parameterBuilder.addExtraQueryParameters(request.extraQueryParameters); } + if (request.stkJwk) { + parameterBuilder.addStkJwkThumbprint(request.stkJwk); + } + return parameterBuilder.createQueryString(); } diff --git a/lib/msal-common/src/crypto/PopTokenGenerator.ts b/lib/msal-common/src/crypto/PopTokenGenerator.ts index 24bb112bd7..7087902a0b 100644 --- a/lib/msal-common/src/crypto/PopTokenGenerator.ts +++ b/lib/msal-common/src/crypto/PopTokenGenerator.ts @@ -18,11 +18,15 @@ import { ClientAuthError } from "../error/ClientAuthError"; * - sw: software storage * - uhw: hardware storage */ -type ReqCnf = { +export type ReqCnf = { kid: string; xms_ksl: KeyLocation; }; +export type StkJwkThumbprint = { + kid: string; +}; + enum KeyLocation { SoftwareStorage = "sw", HardwareStorage = "uhw" diff --git a/lib/msal-common/src/request/RequestParameterBuilder.ts b/lib/msal-common/src/request/RequestParameterBuilder.ts index 0d1f7c06ad..e2f9dd5895 100644 --- a/lib/msal-common/src/request/RequestParameterBuilder.ts +++ b/lib/msal-common/src/request/RequestParameterBuilder.ts @@ -12,6 +12,7 @@ import { LibraryInfo } from "../config/ClientConfiguration"; import { StringUtils } from "../utils/StringUtils"; import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager"; import { ClientInfo } from "../account/ClientInfo"; +import { StkJwkThumbprint } from "../crypto/PopTokenGenerator"; export class RequestParameterBuilder { @@ -383,6 +384,30 @@ export class RequestParameterBuilder { this.parameters.set(AADServerParamKeys.X_MS_LIB_CAPABILITY, ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE); } + /** + * Add stk_jwk thumbprint to query params + * @param stkJwkKid + */ + addStkJwkThumbprint(stkJwkKid: string): void { + if(!StringUtils.isEmpty(stkJwkKid)) { + const stkJwkThumbprint: StkJwkThumbprint = { + kid: stkJwkKid + }; + + this.parameters.set(AADServerParamKeys.STK_JWK, encodeURIComponent(JSON.stringify(stkJwkThumbprint))); + } + } + + /** + * Add stk_jwk public key to query params + * @param stkJwk + */ + addStkJwk(stkJwk: string): void { + if(!StringUtils.isEmpty(stkJwk)) { + this.parameters.set(AADServerParamKeys.STK_JWK, encodeURIComponent(stkJwk)); + } + } + /** * Utility to create a URL from the params map */ diff --git a/lib/msal-common/src/utils/Constants.ts b/lib/msal-common/src/utils/Constants.ts index 57dd627df2..a9d2000cdc 100644 --- a/lib/msal-common/src/utils/Constants.ts +++ b/lib/msal-common/src/utils/Constants.ts @@ -132,6 +132,7 @@ export enum AADServerParamKeys { CLIENT_ASSERTION_TYPE = "client_assertion_type", TOKEN_TYPE = "token_type", REQ_CNF = "req_cnf", + STK_JWK = "stk_jwk", OBO_ASSERTION = "assertion", REQUESTED_TOKEN_USE = "requested_token_use", ON_BEHALF_OF = "on_behalf_of", diff --git a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts index cf280f41ba..82dff83212 100644 --- a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts +++ b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts @@ -63,7 +63,8 @@ describe("AuthorizationCodeClient unit tests", () => { codeChallenge: TEST_CONFIG.TEST_CHALLENGE, codeChallengeMethod: Constants.S256_CODE_CHALLENGE_METHOD, correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER + authenticationScheme: AuthenticationScheme.BEARER, + stkJwk: TEST_POP_VALUES.KID }; const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); expect(loginUrl.includes(Constants.DEFAULT_AUTHORITY)).toBe(true); @@ -75,6 +76,7 @@ describe("AuthorizationCodeClient unit tests", () => { expect(loginUrl.includes(`${AADServerParamKeys.RESPONSE_MODE}=${encodeURIComponent(ResponseMode.QUERY)}`)).toBe(true); expect(loginUrl.includes(`${AADServerParamKeys.CODE_CHALLENGE}=${encodeURIComponent(TEST_CONFIG.TEST_CHALLENGE)}`)).toBe(true); expect(loginUrl.includes(`${AADServerParamKeys.CODE_CHALLENGE_METHOD}=${encodeURIComponent(Constants.S256_CODE_CHALLENGE_METHOD)}`)).toBe(true); + expect(loginUrl.includes(`${AADServerParamKeys.STK_JWK}=${encodeURIComponent(TEST_POP_VALUES.DECODED_STK_JWK_THUMBPRINT)}`)).toBe(true); }); it("Creates an authorization url passing in optional parameters", async () => { @@ -98,7 +100,8 @@ describe("AuthorizationCodeClient unit tests", () => { claims: TEST_CONFIG.CLAIMS, nonce: TEST_CONFIG.NONCE, correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER + authenticationScheme: AuthenticationScheme.BEARER, + stkJwk: TEST_POP_VALUES.KID }; const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); expect(loginUrl.includes(TEST_CONFIG.validAuthority)).toBe(true); @@ -116,6 +119,7 @@ describe("AuthorizationCodeClient unit tests", () => { expect(loginUrl.includes(`${SSOTypes.LOGIN_HINT}=${encodeURIComponent(TEST_CONFIG.LOGIN_HINT)}`)).toBe(true); expect(loginUrl.includes(`${SSOTypes.DOMAIN_HINT}=${encodeURIComponent(TEST_CONFIG.DOMAIN_HINT)}`)).toBe(true); expect(loginUrl.includes(`${AADServerParamKeys.CLAIMS}=${encodeURIComponent(TEST_CONFIG.CLAIMS)}`)).toBe(true); + expect(loginUrl.includes(`${AADServerParamKeys.STK_JWK}=${encodeURIComponent(TEST_POP_VALUES.DECODED_STK_JWK_THUMBPRINT)}`)).toBe(true); }); it("Adds CCS entry if loginHint is provided", async () => { @@ -193,7 +197,8 @@ describe("AuthorizationCodeClient unit tests", () => { correlationId: RANDOM_TEST_GUID, authenticationScheme: AuthenticationScheme.BEARER, authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT + responseMode: ResponseMode.FRAGMENT, + stkJwk: TEST_POP_VALUES.KID }; const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); expect(loginUrl).toEqual(expect.not.arrayContaining([`${SSOTypes.LOGIN_HINT}=`])); @@ -216,7 +221,8 @@ describe("AuthorizationCodeClient unit tests", () => { correlationId: RANDOM_TEST_GUID, authenticationScheme: AuthenticationScheme.BEARER, authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT + responseMode: ResponseMode.FRAGMENT, + stkJwk: TEST_POP_VALUES.KID }; const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); expect(loginUrl.includes(`${SSOTypes.LOGIN_HINT}=${encodeURIComponent(TEST_CONFIG.LOGIN_HINT)}`)).toBe(true); @@ -238,7 +244,8 @@ describe("AuthorizationCodeClient unit tests", () => { correlationId: RANDOM_TEST_GUID, authenticationScheme: AuthenticationScheme.BEARER, authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT + responseMode: ResponseMode.FRAGMENT, + stkJwk: TEST_POP_VALUES.KID }; const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); expect(loginUrl.includes(`${SSOTypes.LOGIN_HINT}=`)).toBe(false); @@ -260,7 +267,8 @@ describe("AuthorizationCodeClient unit tests", () => { correlationId: RANDOM_TEST_GUID, authenticationScheme: AuthenticationScheme.BEARER, authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT + responseMode: ResponseMode.FRAGMENT, + stkJwk: TEST_POP_VALUES.KID }; const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); expect(loginUrl.includes(`${SSOTypes.LOGIN_HINT}=${encodeURIComponent(TEST_CONFIG.LOGIN_HINT)}`)).toBe(true); @@ -299,7 +307,8 @@ describe("AuthorizationCodeClient unit tests", () => { correlationId: RANDOM_TEST_GUID, authenticationScheme: AuthenticationScheme.BEARER, authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT + responseMode: ResponseMode.FRAGMENT, + stkJwk: TEST_POP_VALUES.KID }; const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); expect(loginUrl.includes(`${SSOTypes.SID}=${encodeURIComponent(testTokenClaims.sid)}`)).toBe(true); @@ -336,7 +345,8 @@ describe("AuthorizationCodeClient unit tests", () => { correlationId: RANDOM_TEST_GUID, authenticationScheme: AuthenticationScheme.BEARER, authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT + responseMode: ResponseMode.FRAGMENT, + stkJwk: TEST_POP_VALUES.KID }; const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); expect(loginUrl.includes(`${SSOTypes.SID}=`)).toBe(false); @@ -371,7 +381,8 @@ describe("AuthorizationCodeClient unit tests", () => { correlationId: RANDOM_TEST_GUID, authenticationScheme: AuthenticationScheme.BEARER, authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT + responseMode: ResponseMode.FRAGMENT, + stkJwk: TEST_POP_VALUES.KID }; const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); expect(loginUrl.includes(`${SSOTypes.LOGIN_HINT}=${encodeURIComponent(TEST_CONFIG.LOGIN_HINT)}`)).toBe(true); @@ -392,7 +403,8 @@ describe("AuthorizationCodeClient unit tests", () => { correlationId: RANDOM_TEST_GUID, authenticationScheme: AuthenticationScheme.BEARER, authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT + responseMode: ResponseMode.FRAGMENT, + stkJwk: TEST_POP_VALUES.KID }; const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); expect(loginUrl.includes(`${SSOTypes.LOGIN_HINT}=${encodeURIComponent(TEST_ACCOUNT_INFO.username)}`)).toBe(true); @@ -415,7 +427,8 @@ describe("AuthorizationCodeClient unit tests", () => { correlationId: RANDOM_TEST_GUID, authenticationScheme: AuthenticationScheme.BEARER, authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT + responseMode: ResponseMode.FRAGMENT, + stkJwk: TEST_POP_VALUES.KID }; const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); expect(loginUrl.includes(`${SSOTypes.LOGIN_HINT}=`)).toBe(false); @@ -437,7 +450,8 @@ describe("AuthorizationCodeClient unit tests", () => { correlationId: RANDOM_TEST_GUID, authenticationScheme: AuthenticationScheme.BEARER, authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT + responseMode: ResponseMode.FRAGMENT, + stkJwk: TEST_POP_VALUES.KID }; const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); expect(loginUrl.includes(`${SSOTypes.LOGIN_HINT}=`)).toBe(false); @@ -459,7 +473,8 @@ describe("AuthorizationCodeClient unit tests", () => { correlationId: RANDOM_TEST_GUID, authenticationScheme: AuthenticationScheme.BEARER, authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT + responseMode: ResponseMode.FRAGMENT, + stkJwk: TEST_POP_VALUES.KID }; const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); expect(loginUrl.includes(`${SSOTypes.LOGIN_HINT}=`)).toBe(false); @@ -483,7 +498,8 @@ describe("AuthorizationCodeClient unit tests", () => { correlationId: RANDOM_TEST_GUID, authenticationScheme: AuthenticationScheme.BEARER, authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT + responseMode: ResponseMode.FRAGMENT, + stkJwk: TEST_POP_VALUES.KID }; const loginUrl = await client.getAuthCodeUrl(loginRequest); @@ -611,7 +627,8 @@ describe("AuthorizationCodeClient unit tests", () => { code: "", correlationId: RANDOM_TEST_GUID, authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority + authority: TEST_CONFIG.validAuthority, + stkJwk: TEST_POP_VALUES.KID }; // @ts-ignore await expect(client.acquireToken(codeRequest, null)).rejects.toMatchObject(ClientAuthError.createTokenRequestCannotBeMadeError()); @@ -875,7 +892,8 @@ describe("AuthorizationCodeClient unit tests", () => { codeVerifier: TEST_CONFIG.TEST_VERIFIER, claims: TEST_CONFIG.CLAIMS, correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER + authenticationScheme: AuthenticationScheme.BEARER, + stkJwk: TEST_POP_VALUES.KID }; await client.acquireToken(authCodeRequest, { @@ -951,7 +969,8 @@ describe("AuthorizationCodeClient unit tests", () => { codeVerifier: TEST_CONFIG.TEST_VERIFIER, claims: TEST_CONFIG.CLAIMS, correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER + authenticationScheme: AuthenticationScheme.BEARER, + stkJwk: TEST_POP_VALUES.KID }; const authenticationResult = await client.acquireToken(authCodeRequest, { @@ -989,6 +1008,8 @@ describe("AuthorizationCodeClient unit tests", () => { expect(returnVal.includes(`${AADServerParamKeys.X_CLIENT_CPU}=${TEST_CONFIG.TEST_CPU}`) ).toBe(true); expect(returnVal.includes(`${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE}`)).toBe(true); + expect(returnVal.includes(`${AADServerParamKeys.STK_JWK}=${TEST_POP_VALUES.ENCODED_STK_JWK_THUMBPRINT}`) + ).toBe(true); }); it("Adds tokenQueryParameters to the /token request", () => { @@ -1009,6 +1030,7 @@ describe("AuthorizationCodeClient unit tests", () => { claims: TEST_CONFIG.CLAIMS, correlationId: RANDOM_TEST_GUID, authenticationScheme: AuthenticationScheme.BEARER, + stkJwk: TEST_POP_VALUES.KID, tokenQueryParameters: { testParam: "testValue" } @@ -1189,7 +1211,8 @@ describe("AuthorizationCodeClient unit tests", () => { resourceRequestMethod: "POST", resourceRequestUri: TEST_URIS.TEST_RESOURCE_ENDPT_WITH_PARAMS, claims: TEST_CONFIG.CLAIMS, - correlationId: RANDOM_TEST_GUID + correlationId: RANDOM_TEST_GUID, + stkJwk: TEST_POP_VALUES.KID }; const authenticationResult = await client.acquireToken(authCodeRequest, { @@ -1213,6 +1236,8 @@ describe("AuthorizationCodeClient unit tests", () => { expect(returnVal.includes(`${AADServerParamKeys.TOKEN_TYPE}=${AuthenticationScheme.POP}`)).toBe(true); expect(returnVal.includes(`${AADServerParamKeys.REQ_CNF}=${encodeURIComponent(TEST_POP_VALUES.ENCODED_REQ_CNF)}`)).toBe(true); expect(returnVal.includes(`${AADServerParamKeys.CLAIMS}=${encodeURIComponent(TEST_CONFIG.CLAIMS)}`)).toBe(true); + expect(returnVal.includes(`${AADServerParamKeys.STK_JWK}=${TEST_POP_VALUES.ENCODED_STK_JWK_THUMBPRINT}`)).toBe(true); + }); it("Sends the required parameters when a SSH certificate is requested", async () => { @@ -1488,7 +1513,8 @@ describe("AuthorizationCodeClient unit tests", () => { codeVerifier: TEST_CONFIG.TEST_VERIFIER, claims: TEST_CONFIG.CLAIMS, correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER + authenticationScheme: AuthenticationScheme.BEARER, + stkJwk: TEST_POP_VALUES.KID }; const authenticationResult = await client.acquireToken(authCodeRequest, { @@ -1570,7 +1596,8 @@ describe("AuthorizationCodeClient unit tests", () => { codeVerifier: TEST_CONFIG.TEST_VERIFIER, claims: TEST_CONFIG.CLAIMS, correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER + authenticationScheme: AuthenticationScheme.BEARER, + stkJwk: TEST_POP_VALUES.KID }; const authenticationResult = await client.acquireToken(authCodeRequest, { diff --git a/lib/msal-node/src/crypto/CryptoProvider.ts b/lib/msal-node/src/crypto/CryptoProvider.ts index f2b23e0d73..6c3c3d8569 100644 --- a/lib/msal-node/src/crypto/CryptoProvider.ts +++ b/lib/msal-node/src/crypto/CryptoProvider.ts @@ -94,6 +94,6 @@ export class CryptoProvider implements ICrypto { * @param keyThumbprint */ getAsymmetricPublicKey(): Promise { - throw new Error("Method not implemented"); + throw new Error("Method not implemented."); } } diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/auth.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/auth.js new file mode 100644 index 0000000000..00d1caa114 --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/auth.js @@ -0,0 +1,95 @@ +// Browser check variables +// If you support IE, our recommendation is that you sign-in using Redirect APIs +// If you as a developer are testing using Edge InPrivate mode, please add "isEdge" to the if check +const ua = window.navigator.userAgent; +const msie = ua.indexOf("MSIE "); +const msie11 = ua.indexOf("Trident/"); +const msedge = ua.indexOf("Edge/"); +const isIE = msie > 0 || msie11 > 0; +const isEdge = msedge > 0; + +let signInType; +let accountId = ""; + +// Create the main myMSALObj instance +// configuration parameters are located at authConfig.js +const myMSALObj = new msal.PublicClientApplication(msalConfig); + +// Redirect: once login is successful and redirects with tokens, call Graph API +myMSALObj.handleRedirectPromise().then(handleResponse).catch(err => { + console.error(err); +}); + +function handleResponse(resp) { + if (resp !== null) { + accountId = resp.account.homeAccountId; + myMSALObj.setActiveAccount(resp.account); + showWelcomeMessage(resp.account); + } else { + // need to call getAccount here? + const currentAccounts = myMSALObj.getAllAccounts(); + if (!currentAccounts || currentAccounts.length < 1) { + return; + } else if (currentAccounts.length > 1) { + // Add choose account code here + } else if (currentAccounts.length === 1) { + const activeAccount = currentAccounts[0]; + myMSALObj.setActiveAccount(activeAccount); + accountId = activeAccount.homeAccountId; + showWelcomeMessage(activeAccount); + } + } +} + +async function signIn(method) { + signInType = isIE ? "loginRedirect" : method; + if (signInType === "loginPopup") { + return myMSALObj.loginPopup(loginRequest).then(handleResponse).catch(function (error) { + console.log(error); + }); + } else if (signInType === "loginRedirect") { + return myMSALObj.loginRedirect(loginRequest) + } +} + +function signOut(interactionType) { + const logoutRequest = { + account: myMSALObj.getAccountByHomeId(accountId) + }; + + if (interactionType === "popup") { + myMSALObj.logoutPopup(logoutRequest).then(() => { + window.location.reload(); + }); + } else { + myMSALObj.logoutRedirect(logoutRequest); + } +} + +async function getTokenPopup(request, account) { + return await myMSALObj.acquireTokenSilent(request).catch(async (error) => { + console.log("silent token acquisition fails."); + if (error instanceof msal.InteractionRequiredAuthError) { + console.log("acquiring token using popup"); + return myMSALObj.acquireTokenPopup(request).catch(error => { + console.error(error); + }); + } else { + console.error(error); + } + }); +} + +// This function can be removed if you do not need to support IE +async function getTokenRedirect(request, account) { + return await myMSALObj.acquireTokenSilent(request).catch(async (error) => { + console.log("silent token acquisition fails."); + if (error instanceof msal.InteractionRequiredAuthError) { + // fallback to interaction when silent call fails + console.log("acquiring token using redirect"); + myMSALObj.acquireTokenRedirect(request); + } else { + console.error(error); + } + }); +} diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/authConfig.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/authConfig.js new file mode 100644 index 0000000000..04d19d9129 --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/authConfig.js @@ -0,0 +1,70 @@ +// Config object to be passed to Msal on creation +const msalConfig = { + auth: { + clientId: "bc77b0a7-16aa-4af4-884b-41b968c9c71a", + authority: "https://login.microsoftonline.com/5d97b14d-c396-4aee-b524-c86d33e9b660", + }, + cache: { + cacheLocation: "sessionStorage", // This configures where your cache will be stored + storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge + }, + system: { + loggerOptions: { + loggerCallback: (level, message, containsPii) => { + if (containsPii) { + return; + } + switch (level) { + case msal.LogLevel.Error: + console.error(message); + return; + case msal.LogLevel.Info: + console.info(message); + return; + case msal.LogLevel.Verbose: + console.debug(message); + return; + case msal.LogLevel.Warning: + console.warn(message); + return; + } + }, + logLevel: msal.LogLevel.Verbose + }, + refreshTokenBinding: true + } +}; + +// RT PoP Test Slice params +const extraQueryParams = { + dc: "ESTS-PUB-WUS2-AZ1-TEST1" +}; + +const tokenQueryParams = { + slice: "TestSlice&dc=ESTS-PUB-WUS2-AZ1-TEST1" +} + +// Add here scopes for id token to be used at MS Identity Platform endpoints. +const loginRequest = { + scopes: ["User.Read"], + extraQueryParameters: extraQueryParams, + tokenQueryParameters: tokenQueryParams +}; + +// Add here the endpoints for MS Graph API services you would like to use. +const graphConfig = { + graphMeEndpoint: "https://graph.microsoft-ppe.com/v1.0/me", + graphMailEndpoint: "https://graph.microsoft-ppe.com/v1.0/me/messages" +}; + +// Add here scopes for access token to be used at MS Graph API endpoints. +const tokenRequest = { + scopes: ["Mail.Read"], + forceRefresh: false // Set this to "true" to skip a cached token and go to the server to get a new token +}; + +const silentRequest = { + scopes: ["openid", "profile", "User.Read", "Mail.Read"] +}; + +const logoutRequest = {} diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/graph.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/graph.js new file mode 100644 index 0000000000..0c5f6ba224 --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/graph.js @@ -0,0 +1,64 @@ +// Helper function to call MS Graph API endpoint +// using authorization bearer token scheme +function callMSGraph(endpoint, accessToken, callback) { + const headers = new Headers(); + const bearer = `Bearer ${accessToken}`; + + headers.append("Authorization", bearer); + + const options = { + method: "GET", + headers: headers + }; + + console.log('request made to Graph API at: ' + new Date().toString()); + + fetch(endpoint, options) + .then(response => response.json()) + .then(response => callback(response, endpoint)) + .catch(error => console.log(error)); +} + +async function seeProfile() { + const currentAcc = myMSALObj.getAccountByHomeId(accountId); + if (currentAcc) { + const response = await getTokenPopup(loginRequest, currentAcc).catch(error => { + console.log(error); + }); + callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI); + profileButton.style.display = 'none'; + } +} + +async function readMail() { + const currentAcc = myMSALObj.getAccountByHomeId(accountId); + if (currentAcc) { + const response = await getTokenPopup(tokenRequest, currentAcc).catch(error => { + console.log(error); + }); + callMSGraph(graphConfig.graphMailEndpoint, response.accessToken, updateUI); + mailButton.style.display = 'none'; + } +} + +async function seeProfileRedirect() { + const currentAcc = myMSALObj.getAccountByHomeId(accountId); + if (currentAcc) { + const response = await getTokenRedirect(loginRequest, currentAcc).catch(error => { + console.log(error); + }); + callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI); + profileButton.style.display = 'none'; + } +} + +async function readMailRedirect() { + const currentAcc = myMSALObj.getAccountByHomeId(accountId); + if (currentAcc) { + const response = await getTokenRedirect(tokenRequest, currentAcc).catch(error => { + console.log(error); + }); + callMSGraph(graphConfig.graphMailEndpoint, response.accessToken, updateUI); + mailButton.style.display = 'none'; + } +} diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/index.html b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/index.html new file mode 100644 index 0000000000..f4c2db3ac1 --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/index.html @@ -0,0 +1,70 @@ + + + + + + Quickstart | MSAL.JS Vanilla JavaScript SPA + + + + + + + + + +
+
Vanilla JavaScript SPA calling MS Graph API with MSAL.JS
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/test/browser.spec.ts b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/test/browser.spec.ts new file mode 100644 index 0000000000..35f0fab6b5 --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/test/browser.spec.ts @@ -0,0 +1,73 @@ +import "mocha"; +import puppeteer from "puppeteer"; +import { expect } from "chai"; +import { Screenshot, createFolder, setupCredentials, enterCredentials } from "../../../../../e2eTestUtils/TestUtils"; +import { BrowserCacheUtils } from "../../../../../e2eTestUtils/BrowserCacheTestUtils"; +import { LabApiQueryParams } from "../../../../../e2eTestUtils/LabApiQueryParams"; +import { AzureEnvironments, AppTypes } from "../../../../../e2eTestUtils/Constants"; +import { LabClient } from "../../../../../e2eTestUtils/LabClient"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots`; +const SAMPLE_HOME_URL = "http://localhost:30662/"; +let username = ""; +let accountPwd = ""; + +describe("Browser tests", function () { + this.timeout(0); + this.retries(1); + + let browser: puppeteer.Browser; + before(async () => { + createFolder(SCREENSHOT_BASE_FOLDER_NAME); + const labApiParams: LabApiQueryParams = { + azureEnvironment: AzureEnvironments.PPE, + appType: AppTypes.CLOUD + }; + + const labClient = new LabClient(); + const envResponse = await labClient.getVarsByCloudEnvironment(labApiParams); + [username, accountPwd] = await setupCredentials(envResponse[0], labClient); + + browser = await puppeteer.launch({ + headless: true, + ignoreDefaultArgs: ["--no-sandbox", "–disable-setuid-sandbox"] + }); + }); + + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + let BrowserCache: BrowserCacheUtils; + beforeEach(async () => { + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + BrowserCache = new BrowserCacheUtils(page, "sessionStorage"); + await page.goto(SAMPLE_HOME_URL); + }); + + afterEach(async () => { + await page.close(); + }); + + after(async () => { + await context.close(); + await browser.close(); + }); + + it("Performs loginRedirect with RT binding", async () => { + const testName = "redirectBaseCase"; + const screenshot = new Screenshot(`${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`); + // Home Page + await page.waitForSelector("#SignIn"); + await screenshot.takeScreenshot(page, "samplePageInit"); + // Click Sign In + await page.click("#SignIn"); + await page.waitForSelector("#loginRedirect"); + await screenshot.takeScreenshot(page, "signInClicked"); + // Click Sign In With Redirect + await page.click("#loginRedirect"); + // Check URL to see if it contains stkJwk + await page.waitForNavigation({ waitUntil: "networkidle0", timeout: 10000}); + await page.waitForSelector("input#i0116.input.text-box"); + expect(page.url()).to.include("stk_jwk"); + }); +}); diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/ui.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/ui.js new file mode 100644 index 0000000000..a9a6b084bc --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/boundRt/ui.js @@ -0,0 +1,70 @@ +// Select DOM elements to work with +const welcomeDiv = document.getElementById("WelcomeMessage"); +const signInButton = document.getElementById("SignIn"); +const popupButton = document.getElementById("loginPopup"); +const redirectButton = document.getElementById("loginRedirect"); +const cardDiv = document.getElementById("card-div"); +const mailButton = document.getElementById("readMail"); +const profileButton = document.getElementById("seeProfile"); +const profileDiv = document.getElementById("profile-div"); + +function showWelcomeMessage(account) { + // Reconfiguring DOM elements + cardDiv.style.display = 'initial'; + welcomeDiv.innerHTML = `Welcome ${account.username}`; + signInButton.setAttribute('class', "btn btn-success dropdown-toggle"); + signInButton.innerHTML = "Sign Out"; + popupButton.setAttribute('onClick', "signOut(this.id)"); + popupButton.innerHTML = "Sign Out with Popup"; + redirectButton.setAttribute('onClick', "signOut(this.id)"); + redirectButton.innerHTML = "Sign Out with Redirect"; +} + +function updateUI(data, endpoint) { + console.log('Graph API responded at: ' + new Date().toString()); + + if (endpoint === graphConfig.graphMeEndpoint) { + const title = document.createElement('p'); + title.innerHTML = "Title: " + data.jobTitle; + const email = document.createElement('p'); + email.innerHTML = "Mail: " + data.mail; + const phone = document.createElement('p'); + phone.innerHTML = "Phone: " + data.businessPhones[0]; + const address = document.createElement('p'); + address.innerHTML = "Location: " + data.officeLocation; + profileDiv.appendChild(title); + profileDiv.appendChild(email); + profileDiv.appendChild(phone); + profileDiv.appendChild(address); + } else if (endpoint === graphConfig.graphMailEndpoint) { + if (data.value.length < 1) { + alert("Your mailbox is empty!") + } else { + const tabList = document.getElementById("list-tab"); + const tabContent = document.getElementById("nav-tabContent"); + + data.value.map((d, i) => { + // Keeping it simple + if (i < 10) { + const listItem = document.createElement("a"); + listItem.setAttribute("class", "list-group-item list-group-item-action") + listItem.setAttribute("id", "list" + i + "list") + listItem.setAttribute("data-toggle", "list") + listItem.setAttribute("href", "#list" + i) + listItem.setAttribute("role", "tab") + listItem.setAttribute("aria-controls", i) + listItem.innerHTML = d.subject; + tabList.appendChild(listItem) + + const contentItem = document.createElement("div"); + contentItem.setAttribute("class", "tab-pane fade") + contentItem.setAttribute("id", "list" + i) + contentItem.setAttribute("role", "tabpanel") + contentItem.setAttribute("aria-labelledby", "list" + i + "list") + contentItem.innerHTML = " from: " + d.from.emailAddress.address + "

" + d.bodyPreview + "..."; + tabContent.appendChild(contentItem); + } + }); + } + } +}