diff --git a/change/msal-2020-08-10-11-48-42-scopes-and-response-types.json b/change/msal-2020-08-10-11-48-42-scopes-and-response-types.json new file mode 100644 index 0000000000..fd16362b37 --- /dev/null +++ b/change/msal-2020-08-10-11-48-42-scopes-and-response-types.json @@ -0,0 +1,8 @@ +{ + "type": "minor", + "comment": "Enables idToken acquisition in acquireToken API calls through the use of OIDC scopes by redefining the way response_type is determined. (PR #2022)", + "packageName": "msal", + "email": "hemoral@microsoft.com", + "dependentChangeType": "patch", + "date": "2020-08-10T18:48:42.205Z" +} diff --git a/change/msal-2020-08-12-11-09-30-msal-core-adfs.json b/change/msal-2020-08-12-11-09-30-msal-core-adfs.json new file mode 100644 index 0000000000..b98d763cff --- /dev/null +++ b/change/msal-2020-08-12-11-09-30-msal-core-adfs.json @@ -0,0 +1,8 @@ +{ + "type": "minor", + "comment": "ADFS 2019 Support (#1668)", + "packageName": "msal", + "email": "thomas.l.norling@gmail.com", + "dependentChangeType": "patch", + "date": "2020-08-12T18:09:30.073Z" +} diff --git a/change/msal-2020-08-12-14-47-04-b2c-multiple-policies.json b/change/msal-2020-08-12-14-47-04-b2c-multiple-policies.json new file mode 100644 index 0000000000..0f28688b1f --- /dev/null +++ b/change/msal-2020-08-12-14-47-04-b2c-multiple-policies.json @@ -0,0 +1,8 @@ +{ + "type": "minor", + "comment": "B2C Multiple Policy Support (#1757)", + "packageName": "msal", + "email": "thomas.norling@microsoft.com", + "dependentChangeType": "patch", + "date": "2020-08-12T21:47:04.427Z" +} diff --git a/lib/msal-core/README.md b/lib/msal-core/README.md index 67219ad9ae..9a7130426f 100644 --- a/lib/msal-core/README.md +++ b/lib/msal-core/README.md @@ -19,18 +19,18 @@ MSAL for JavaScript enables client-side JavaScript web applications, running in #### Latest compiled and minified JavaScript (US West region) ```html - + ``` ```html - + ```` #### Alternate region URLs (Europe region) ```html - + ``` ```html - + ``` ### Via Latest Microsoft CDN Version (with SRI Hash): @@ -38,10 +38,10 @@ MSAL for JavaScript enables client-side JavaScript web applications, running in #### Latest compiled and minified JavaScript ```html - + ``` ```html - + ``` #### Alternate region URLs @@ -49,18 +49,18 @@ MSAL for JavaScript enables client-side JavaScript web applications, running in To help ensure reliability, Microsoft provides a second CDN: ```html - + ``` ```html - + ``` Below is an example of how to use one CDN as a fallback when the other CDN is not working: ```html - + ``` diff --git a/lib/msal-core/docs/FAQ.md b/lib/msal-core/docs/FAQ.md index 1b33620479..bda8092047 100644 --- a/lib/msal-core/docs/FAQ.md +++ b/lib/msal-core/docs/FAQ.md @@ -51,6 +51,7 @@ **[Common Issues](#common-issues)** 1. [How to avoid page reloads when acquiring and renewing tokens silently?](#how-to-avoid-page-reloads-when-acquiring-and-renewing-tokens-silently) 1. [Why is my application stuck in an infinite redirect loop?](#why-is-my-application-stuck-in-an-infinite-redirect-loop) +1. [I'm using one of your samples on Internet Explorer and I get the error SignIn() is not defined](#im-using-one-of-your-samples-on-internet-explorer-and-i-get-the-error-signin-is-not-defined) 1. [Why is MSAL throwing an error?](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-core/docs/errors.md) *** @@ -346,25 +347,25 @@ Please see the documentation on [Tenancy in Azure Active Directory](https://docs ## My B2C application has more than one user-flow/policy. How do I work with multiple policies in MSAL.js? -Unfortunately, MSAL.js does not support multiple B2C policies _out-of-the-box_ at this moment. Nevertheless, you can still utilize the library to workaround this limitation. For instance, review our sample [here](https://github.com/Azure-Samples/active-directory-b2c-javascript-msal-singlepageapp) to see how to implement **sign-up/sign-in** and **password reset** user flows. +MSAL.js allows you to provide an authority on a per-request basis. To acquire an access token for a different policy than the one you signed in with, simply pass the relevant authority as a part of the request object. -## How can I implement password reset user flow in my B2C application with MSAL.js? +```javascript +const request = { +scopes: ["https://b2ctenant.onmicrosoft.com/exampleApi/exampleScope"], +authority: "https://b2ctenant.b2clogin.com/b2ctenant.onmicrosoft.com/examplePolicy" +} -Please review our sample [here](https://github.com/Azure-Samples/active-directory-b2c-javascript-msal-singlepageapp) to see how to implement the **password reset** user flow. +msal.acquireTokenPopup(request); +``` -## I'm using one of your samples on Internet Explorer and I get the error "SignIn() is not defined" +A few additional things to keep in mind regarding multiple policy scenarios: -Our samples use [ES6](http://www.ecma-international.org/ecma-262/6.0/) conventions, in particular **promises**, **arrow functions** and **template literals**. As such, they will **not** work on Internet Explorer out-of-the-box. For **promises**, you need to add a polyfill, i.e.: +- MSAL.js 1.x is only able to cache one id_token at a time, which means that obtaining an id_token for a different policy will overwrite the cached id_token from the previous policy. +- Some policies, such as profile_edit and password_reset, require interaction and cannot be used to renew tokens silently. Obtaining a cached access token via `acquireTokenSilent` is still possible, however, if the token is expired the service will throw an "X-Frame Options DENY" error when MSAL attempts to renew it. When this happens your application must catch this error and fallback to calling an interactive method (`acquireTokenRedirect` or `acquireTokenPopup`) -```html - - - - -``` +## How can I implement password reset user flow in my B2C application with MSAL.js? -For **arrow functions** and **template literals**, you need to transpile them to old JavaScript. You can use [this tool](https://babeljs.io/repl) to help with the process. +Please checkout our sample [here](https://github.com/Azure-Samples/active-directory-b2c-javascript-msal-singlepageapp) to see how to implement the **password reset** user flow. # Common Issues @@ -534,3 +535,17 @@ msal = new Msal.UserAgentApplication({ } }) ``` + +## I'm using one of your samples on Internet Explorer and I get the error "SignIn() is not defined" + +Our samples use [ES6](http://www.ecma-international.org/ecma-262/6.0/) conventions, in particular **promises**, **arrow functions** and **template literals**. As such, they will **not** work on Internet Explorer out-of-the-box. For **promises**, you need to add a polyfill, i.e.: + +```html + + + + +``` + +For **arrow functions** and **template literals**, you need to transpile them to old JavaScript. You can use [this tool](https://babeljs.io/repl) to help with the process. diff --git a/lib/msal-core/docs/response-types.md b/lib/msal-core/docs/response-types.md new file mode 100644 index 0000000000..e8ec4eb7cb --- /dev/null +++ b/lib/msal-core/docs/response-types.md @@ -0,0 +1,85 @@ +# Response Types + +> :warning: This document only applies to `msal@1.x` which implements the Implicit Flow Grant type. For the Authorization Code Flow Grant type, please use the [msal-browser](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser) library. + +## Quick Reference + +> This section provides a summary of the main points this document addresses, without getting into any details. If you need more clarity or information about the functionality and behavior of Response Types in `msal@1.x`, please read the rest of this document. + +The key takeaways of the way `msal@1.x` determines and handles `Response Types` are: + +1. `loginRedirect`, `loginPopup` and `ssoSilent` will always return ID tokens and have a `response_type` of `id_token`. +2. `acquireToken` requests will always return an ID token if `openid` or `profile` are included in the request scopes. +3. `acquireToken` requests will always return an access token if a `resource scope` is requested. + +If you're interested in learning more about the reasoning and implications around the way response types are determined and what they are used for, please read the rest of this document. + +## Definition and Types +The `msal@1.x` library, in compliance of both the OAuth 2.0 protocol specification as well as the OpenID Connect specification, defines and supports three different `response types`: + +* token +* id_token +* id_token token + +The **`msal@1.x` library does not support the `code` response type because it does not implement the Authorization Code grant. If you are looking to implement the Authorization Code grant type, consider the [msal-browser](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser) library.** + +The listed response types are possible values for the `response_type` parameter in OAuth 2.0 HTTP requests. Assuming a valid request, this parameter determines what kind of token is sent back by the Secure Token Service (STS) that `msal@1.x` requests access and ID tokens from. + +| Response Type | Specification that defines it | Expected token type from successful request | Action | +| ------------- | ----------------------------- | ------------------------------------------- | ------ | +| `token` |[OAuth 2.0](https://tools.ietf.org/html/rfc6749#section-3.1.1) | Access Token | Authorization | +| `id_token`| [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html#Authentication) | ID Token | Authentication | +|`id_token token`| [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html#Authentication) | Access Token and ID token | Authorization & Authentication | + +**Note: Given that `msal@1.x` uses the OAuth 2.0 Implicit Flow exclusively, which leverages URL fragments for token reception, it is important to be mindful of URL length limitations. Browsers like IE impose restrictions on the length of URLs, so getting both an access token and ID token in the same URL may cause unexpected or incorrect behavior.** + +## Response Type configuration and behavior + +The `response_type` attribute presented above cannot be configured directly. However, it is important to understand the way `msal@1.x` determines which response type is set and, therefore, what kind of token the developer can expect for each scenario. The factors that come into consideration when setting the request's `response_type` parameter are the following: + +1. The `msal@1.x` API called +2. Whether the account passed into the request configuration matches the account in the MSAL cache +3. The contents of the `scopes` array in the Authorization Request Configuration object. For more information on `scopes` configuration, please consult the [Scopes](/docs/scopes.md) document. + +**Important note: Login APIs will always set `response_type=id_token`, given that they are designed to perform user login (authentication).** + +Login APIs include: + +* loginRedirect +* loginPopup +* ssoSilent + +In other words, whenever you call `loginRedirect` or `loginPopup` to sign a user in, you should expect to receive an ID token if the request is successful. + +The following section contains quick reference tables for both `login` and `acquireToken` APIs that accurately map the request configuration to the resulting response type. + +## Quick reference tables + +### Login APIs + +Applies to: `loginRedirect`, `loginPopup`, `ssoSilent` + +| Input scopes | Account passed in | Response Type Result | +| ----------------- | ------------ | -------------------- | +| Any case | Any case | `id_token`| + +### Acquire Token APIs + +Applies to: `acquireTokenRedirect`, `acquireTokenPopup`, `acquireTokenSilent` + +* *OIDC scopes: any combination of `openid` and/or `profile`* +* *OIDC scopes only: Same as OIDC scopes but with no other scopes in the array* + +| Input scopes | Account passed in | Response Type Result | +| ----------------- | ------------ | -------------------- | +| ClientId as only scope | Any case | `id_token`| +| OIDC scopes only | Any case | `id_token`| +| ClientId with OIDC scopes | Any case | `id_token token` | +| Resource scope(s) with OIDC scopes (technically the same as above) | Any case | `id_token token` | +| Resource scope(s) only | Matches cached account object | `token` | +| Resource scope(s) and ClientId | Matches cached account object | `token` | +| Resource scope(s) only | Doesn't match cached account object | `id_token token` | + +**Note: As seen in the table above, when ClientId is not the only scope, it is assumed to be a resource scope with no special behavior.** + + diff --git a/lib/msal-core/docs/scopes.md b/lib/msal-core/docs/scopes.md new file mode 100644 index 0000000000..bbd3adf672 --- /dev/null +++ b/lib/msal-core/docs/scopes.md @@ -0,0 +1,105 @@ +# Scope Configuration and Behavior + +## Contents +* [Quick Reference](#quick-reference) +* [Scopes](#scopes) + * [Scope Functions](#scope-functions) + * [Scope Types](#scope-types) + * [Resource scopes for Authorization](#resource-scopes-for-authorization) + * [OpenID Connect Scopes for Authentication](#openid-connect-scopes-for-authentication) +* [Scopes Behavior](#scopes-behavior) + * [Default Scopes in Authorization Requests](#default-scopes-on-authorization-requests) + * [Special OIDC Scopes behavior cases](#special-oidc-scopes-behavior-cases) + +## Quick Reference + +> This section provides a summary of the main points this document addresses, without getting into any details. If you need more clarity or information about the functionality and behavior of Scopes in `msal@1.x`, please read the rest of this document. + +The key takeaways of the way `msal@1.x` handles and uses scopes are: + +1. The `msal@1.x` library will always append `openid` and `profile` as scopes in every outgoing request. +2. Setting the value of the application's ClientId as the only scope will result in it being replaced by `openid` and `profile` and an ID Token being returned + +If you're interested in learning more about the reasoning and implications around these two specific behaviors, please read on. + + + +## Scopes + +Microsoft identity platform access tokens, which `msal@1.x` acquires in compliance with the OAuth 2.0 protocol specification, are issued to applications as proof of authorization on behalf of a user for a certain resource. The issuing of these tokens is not only specific to an `audience`, or application, but also specific to a set of `scopes` or permissions. + +### Scope Functions + +#### Function of scopes in OAuth 2.0 + +The main function of the `scopes` configuration, per the [OAuth 2.0 Access Token Scope Reference](https://tools.ietf.org/html/rfc6749#section-3.3), is to determine the permissions for which an application requests `authorization` on behalf of the user. Said function is both supported and covered by `msal@1.x` and the Microsoft identity platform in general. For more information on the regular function of authorization scopes, please consult the official [Microsoft identity platform documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent). + +#### Special use of scopes in msal@1.x + +In addition to the global concept and use of `scopes`, it is important to understand that `msal@1.x` gives scopes a special use that adds to the importance of their configuration. In short, `msal@1.x` allows developers to leverage certain scopes in order to determine the `response_type` for the final request. For more information on the way the scopes configuration determines the `response_type` parameter, please refer to the [Response Types Document](/docs/response-types.md). + + + +## Scope Types + +As far as `msal@1.x` is concerned, there are two main types of `scopes` that can be configured in a token request. + +### Resource scopes for Authorization + +`Resource scopes` are the main type of access token `scopes` that `msal@1.x` deals with. These are the `scopes` that represent permissions for specific actions against a particular resource. In other words, these `scopes` determine what actions and resources the requesting application is `authorized` to access on behalf of the user. The following are some examples of the `resource scopes` that the [Microsoft Graph](https://docs.microsoft.com/en-us/graph/overview) service can authorize an application for given the user's consent: + +* `User.Read`: Authorizes the application to read a user's account details. +* `Mail.Read`: Authorizes the application to read a user's e-mails. + +Including resource scopes in the configuration for a token request doesn't always mean that the response will include an **access token** for said scopes. In the specific case of `msal@1.x`'s `login` APIs (`loginRedirect`, `loginPopup`), adding resource scopes may allow the user to **constent** to said scopes ahead of time, but successful `login` API calls always result in an **ID Token, not an access token**, being returned. + +### OpenID Connect Scopes for Authentication + +`OpenID Connect (OIDC) scopes` are a specific set of scopes that can be added to requests when `authenticating` a user. In most cases, `OIDC scopes` are added to configure the claims included in an ID Token ([OIDC Reference](https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) / [Microsoft Docs](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes)). Some services, such as the Secure Token Service that `msal@1.x` acquires tokens from, also use OIDC scopes in their internal logic. For this reason, it is important to understand and pay attention to the special behavior `msal@1.x` has around OIDC scopes (described in the [next section](#default-scopes-on-authorization-requests)). + +The OIDC scopes that `msal@1.x` pays particular attention to are outlined in the table below. + +| OIDC Scope | Required by OIDC Specification | Function | OIDC Reference | Microsoft Docs | +| ---------- | ------------------------------ | -------- | -------------- | -------------- | +| `openid`| Mandatory | Main `OIDC scope` that indicates a request for `authentication` [per the OIDC specification](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). In AAD requests, this is the scope that prompts the "Sign in" permission that a user can consent to. | [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest)| [Permissions and consent in the Microsoft identity platform endpoint](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc#send-the-sign-in-request)| +|`profile`| Optional | Used for ID Token `claims` configuration. Adds the end-user's default profile information as a claim to the ID token returned | [Requesting Claims using Scopes Values](https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) | [OpenID Permissions](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent)| + + +## Scopes Behavior + +### Default Scopes on Authorization Requests + +Understanding how `OIDC scopes` configure the claims included in an authentication response's ID Token is important when using `msal@1.x` to acquire said ID Tokens. However, there is an important note to be made on how the `openid` and `profile` scopes are added by `msal@1.x` to all server requests by default that does not directly relate to the OpenID Connect specification. + +Like previously mentioned, the Secure Token Service that `msal@1.x` requests access and ID tokens from also makes use of the `openid` and `profile` scopes. Specifically, the STS expects these two scopes in order to configure and provide the `client_info` parameter in authorization and authentication responses. The `msal@1.x` library depends on the contents of `client_info` in order to successfully cache tokens and, therefore, provide silent token acquisition as a feature. + +**For this reason, whether or not the developer adds the `openid` or `profile` scopes to their request configuration, `msal@1.x` will make sure they are included before sending the request to the STS.** + +### Special OIDC Scopes behavior cases + +The following is a list of practical implications and examples of the default scope behavior described in the previous section. + +- If the scopes array does not include either `openid` or `profile`, whichever is missing (could be both) will be added to the scopes array by default before the request is sent out. + + Examples: + + ```js + { scopes: ['User.Read'] } // becomes { scopes: ['User.Read', 'openid', 'profile'] } before the request is sent + + { scopes: ['User.Read', 'openid'] } // becomes { scopes ['User.Read', 'openid', 'profile']} before the request is sent + + { scopes: ['User.Read', 'profile'] } // becomes { scopes ['User.Read', 'profile', 'openid']} before the request is sent + + { scopes: ['http://contoso.com/scope'] } // becomes { scopes ['http://contoso.com/scope', 'openid', 'profile'] } + ``` +- ClientId is removed from the scopes array when it is the only scope in the configuration. If it is not the only scope, it is treated as a resource scope and will be sent in the final server request. + + Examples: + + ```js + { scopes: ['YOUR_CLIENT_ID'] } // becomes { scopes: ['openid', 'profile'] } before the request is sent (ClientId is spliced out) + + { scopes: ['YOUR_CLIENT_ID', 'User.Read'] } // becomes { scopes ['YOUR_CLIENT_ID', 'User.Read', 'openid', 'profile']} before the request is sent (ClientId is treated as resource scope and therefore not spliced out) + + { scopes: ['YOUR_CLIENT_ID', 'openid'] } // becomes { scopes ['YOUR_CLIENT_ID', 'openid', 'profile']} before the request is sent + ``` \ No newline at end of file diff --git a/lib/msal-core/package-lock.json b/lib/msal-core/package-lock.json index 2575f86aba..00864c08ee 100644 --- a/lib/msal-core/package-lock.json +++ b/lib/msal-core/package-lock.json @@ -3593,7 +3593,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { @@ -3630,7 +3630,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { @@ -4144,7 +4144,7 @@ }, "concat-stream": { "version": "1.6.2", - "resolved": "http://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "dev": true, "requires": { @@ -4284,7 +4284,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "requires": { @@ -4297,7 +4297,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "requires": { @@ -4522,7 +4522,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, "requires": { @@ -6400,7 +6400,7 @@ }, "htmlparser2": { "version": "3.8.3", - "resolved": "http://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", "dev": true, "requires": { @@ -6419,7 +6419,7 @@ }, "readable-stream": { "version": "1.1.14", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { @@ -6431,7 +6431,7 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true } @@ -7971,7 +7971,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { @@ -8364,7 +8364,7 @@ }, "os-tmpdir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, @@ -8523,7 +8523,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -8832,7 +8832,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -9191,7 +9191,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { @@ -9303,7 +9303,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { @@ -9832,7 +9832,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -9856,7 +9856,7 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, @@ -10142,7 +10142,7 @@ }, "through2": { "version": "2.0.1", - "resolved": "http://registry.npmjs.org/through2/-/through2-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz", "integrity": "sha1-OE51MU1J8y3hLuu4E2uOtrXVnak=", "dev": true, "requires": { @@ -10158,7 +10158,7 @@ }, "readable-stream": { "version": "2.0.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", "dev": true, "requires": { @@ -10172,7 +10172,7 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true } diff --git a/lib/msal-core/src/Account.ts b/lib/msal-core/src/Account.ts index be30c53e4d..8a88273905 100644 --- a/lib/msal-core/src/Account.ts +++ b/lib/msal-core/src/Account.ts @@ -66,8 +66,8 @@ export class Account { const utid: string = clientInfo ? clientInfo.utid : ""; let homeAccountIdentifier: string; - if (!StringUtils.isEmpty(uid) && !StringUtils.isEmpty(utid)) { - homeAccountIdentifier = CryptoUtils.base64Encode(uid) + "." + CryptoUtils.base64Encode(utid); + if (!StringUtils.isEmpty(uid)) { + homeAccountIdentifier = StringUtils.isEmpty(utid)? CryptoUtils.base64Encode(uid): CryptoUtils.base64Encode(uid) + "." + CryptoUtils.base64Encode(utid); } return new Account(accountIdentifier, homeAccountIdentifier, idToken.preferredName, idToken.name, idToken.claims, idToken.sid, idToken.issuer); } diff --git a/lib/msal-core/src/ClientInfo.ts b/lib/msal-core/src/ClientInfo.ts index da625adf43..55548e3957 100644 --- a/lib/msal-core/src/ClientInfo.ts +++ b/lib/msal-core/src/ClientInfo.ts @@ -6,6 +6,7 @@ import { CryptoUtils } from "./utils/CryptoUtils"; import { ClientAuthError } from "./error/ClientAuthError"; import { StringUtils } from "./utils/StringUtils"; +import { IdToken } from "./IdToken"; /** * @hidden @@ -30,7 +31,16 @@ export class ClientInfo { this._utid = utid; } - constructor(rawClientInfo: string) { + static createClientInfoFromIdToken(idToken:IdToken, authority: string): ClientInfo { + const clientInfo = { + uid: idToken.subject, + utid: "" + }; + + return new ClientInfo(CryptoUtils.base64Encode(JSON.stringify(clientInfo)), authority); + } + + constructor(rawClientInfo: string, authority: string) { if (!rawClientInfo || StringUtils.isEmpty(rawClientInfo)) { this.uid = ""; this.utid = ""; @@ -42,7 +52,7 @@ export class ClientInfo { const clientInfo: ClientInfo = JSON.parse(decodedClientInfo); if (clientInfo) { if (clientInfo.hasOwnProperty("uid")) { - this.uid = clientInfo.uid; + this.uid = authority ? ClientInfo.stripPolicyFromUid(clientInfo.uid, authority): clientInfo.uid; } if (clientInfo.hasOwnProperty("utid")) { @@ -53,4 +63,31 @@ export class ClientInfo { throw ClientAuthError.createClientInfoDecodingError(e); } } + + static stripPolicyFromUid(uid: string, authority: string): string { + const uidSegments = uid.split("-"); + // Reverse the url segments so the last one is more easily accessible + const urlSegments = authority.split("/").reverse(); + let policy = ""; + + if (!StringUtils.isEmpty(urlSegments[0])) { + policy = urlSegments[0]; + } else if (urlSegments.length > 1) { + // If the original url had a trailing slash, urlSegments[0] would be "" so take the next element + policy = urlSegments[1]; + } + + if (uidSegments[uidSegments.length - 1] === policy) { + // If the last segment of uid matches the last segment of authority url, remove the last segment of uid + return uidSegments.slice(0, uidSegments.length - 1).join("-"); + } + + return uid; + } + + public encodeClientInfo() { + const clientInfo = JSON.stringify({uid: this.uid, utid: this.utid}); + + return CryptoUtils.base64Encode(clientInfo); + } } diff --git a/lib/msal-core/src/IdToken.ts b/lib/msal-core/src/IdToken.ts index 248b997e5e..d0945ecb7c 100644 --- a/lib/msal-core/src/IdToken.ts +++ b/lib/msal-core/src/IdToken.ts @@ -58,6 +58,8 @@ export class IdToken { if (this.claims.hasOwnProperty("preferred_username")) { this.preferredName = this.claims["preferred_username"]; + } else if (this.claims.hasOwnProperty("upn")) { + this.preferredName = this.claims["upn"]; } if (this.claims.hasOwnProperty("name")) { diff --git a/lib/msal-core/src/ScopeSet.ts b/lib/msal-core/src/ScopeSet.ts index 875020c71d..9a52950ba5 100644 --- a/lib/msal-core/src/ScopeSet.ts +++ b/lib/msal-core/src/ScopeSet.ts @@ -114,16 +114,9 @@ export class ScopeSet { } // Check that scopes is not an empty array - if (scopes.length < 1) { + if (scopes.length < 1 && scopesRequired) { throw ClientConfigurationError.createEmptyScopesArrayError(scopes.toString()); } - - // Check that clientId is passed as single scope - if (scopes.indexOf(clientId) > -1) { - if (scopes.length > 1) { - throw ClientConfigurationError.createClientIdSingleScopeError(scopes.toString()); - } - } } /** @@ -160,4 +153,70 @@ export class ScopeSet { // #endregion + /** + * @ignore + * Returns true if the scopes array only contains openid and/or profile + */ + static onlyContainsOidcScopes(scopes: Array): boolean { + const scopesCount = scopes.length; + let oidcScopesFound = 0; + + if (scopes.indexOf(Constants.openidScope) > -1) { + oidcScopesFound += 1; + } + + if (scopes.indexOf(Constants.profileScope) > -1) { + oidcScopesFound += 1; + } + + return (scopesCount > 0 && scopesCount === oidcScopesFound); + } + + /** + * @ignore + * Returns true if the scopes array only contains openid and/or profile + */ + static containsAnyOidcScopes(scopes: Array): boolean { + const containsOpenIdScope = scopes.indexOf(Constants.openidScope) > -1; + const containsProfileScope = scopes.indexOf(Constants.profileScope) > -1; + + return (containsOpenIdScope || containsProfileScope); + } + + /** + * @ignore + * Returns true if the clientId is the only scope in the array + */ + static onlyContainsClientId(scopes: Array, clientId: string): boolean { + // Double negation to force false value returned in case scopes is null + return !!scopes && (scopes.indexOf(clientId) > -1 && scopes.length === 1); + } + + /** + * @ignore + * Adds missing OIDC scopes to scopes array withot duplication. Since STS requires OIDC scopes for + * all implicit flow requests, 'openid' and 'profile' should always be included in the final request + */ + static appendDefaultScopes(scopes: Array): Array { + const extendedScopes = scopes; + if (extendedScopes.indexOf(Constants.openidScope) === -1) { + extendedScopes.push(Constants.openidScope); + } + + if(extendedScopes.indexOf(Constants.profileScope) === -1) { + extendedScopes.push(Constants.profileScope); + } + + return extendedScopes; + } + + /** + * @ignore + * Removes clientId from scopes array if included as only scope. If it's not the only scope, it is treated as a resource scope. + * @param scopes Array: Pre-normalized scopes array + * @param clientId string: The application's clientId that is searched for in the scopes array + */ + static translateClientIdIfSingleScope(scopes: Array, clientId: string): Array { + return this.onlyContainsClientId(scopes, clientId) ? Constants.oidcScopes : scopes; + } } diff --git a/lib/msal-core/src/ServerRequestParameters.ts b/lib/msal-core/src/ServerRequestParameters.ts index 9355187360..66f70b6a4a 100644 --- a/lib/msal-core/src/ServerRequestParameters.ts +++ b/lib/msal-core/src/ServerRequestParameters.ts @@ -8,7 +8,7 @@ import { CryptoUtils } from "./utils/CryptoUtils"; import { AuthenticationParameters } from "./AuthenticationParameters"; import { StringDict } from "./MsalTypes"; import { Account } from "./Account"; -import { SSOTypes, Constants, PromptState, libraryVersion } from "./utils/Constants"; +import { SSOTypes, Constants, PromptState, libraryVersion, ResponseTypes } from "./utils/Constants"; import { StringUtils } from "./utils/StringUtils"; import { ScopeSet } from "./ScopeSet"; @@ -59,7 +59,7 @@ export class ServerRequestParameters { this.nonce = CryptoUtils.createNewGuid(); // set scope to clientId if null - this.scopes = scopes ? [ ...scopes] : [clientId]; + this.scopes = scopes ? [ ...scopes] : Constants.oidcScopes; this.scopes = ScopeSet.trimScopes(this.scopes); // set state (already set at top level) @@ -277,4 +277,29 @@ export class ServerRequestParameters { static isSSOParam(request: AuthenticationParameters) { return request && (request.account || request.sid || request.loginHint); } + + /** + * Returns the correct response_type string attribute for an acquireToken request configuration + * @param accountsMatch boolean: Determines whether the account in the request matches the cached account + * @param scopes Array: AuthenticationRequest scopes configuration + * @param loginScopesOnly boolean: True if the scopes array ONLY contains the clientId or any combination of OIDC scopes, without resource scopes + */ + static determineResponseType(accountsMatch: boolean, scopes: Array) { + // Supports getting an id_token by sending in clientId as only scope or OIDC scopes as only scopes + if (ScopeSet.onlyContainsOidcScopes(scopes)) { + return ResponseTypes.id_token; + } + // If accounts match, check if OIDC scopes are included, otherwise return id_token_token + return (accountsMatch) ? this.responseTypeForMatchingAccounts(scopes) : ResponseTypes.id_token_token; + } + + /** + * Returns the correct response_type string attribute for an acquireToken request configuration that contains an + * account that matches the account in the MSAL cache. + * @param scopes Array: AuthenticationRequest scopes configuration + */ + private static responseTypeForMatchingAccounts(scopes: Array): string { + // Opt-into also requesting an ID token by sending in 'openid', 'profile' or both along with resource scopes when login is not necessary. + return (ScopeSet.containsAnyOidcScopes(scopes)) ? ResponseTypes.id_token_token : ResponseTypes.token; + } } diff --git a/lib/msal-core/src/UserAgentApplication.ts b/lib/msal-core/src/UserAgentApplication.ts index c697353c36..8965cbcd36 100644 --- a/lib/msal-core/src/UserAgentApplication.ts +++ b/lib/msal-core/src/UserAgentApplication.ts @@ -7,7 +7,7 @@ import { AccessTokenCacheItem } from "./cache/AccessTokenCacheItem"; import { AccessTokenKey } from "./cache/AccessTokenKey"; import { AccessTokenValue } from "./cache/AccessTokenValue"; import { ServerRequestParameters } from "./ServerRequestParameters"; -import { Authority } from "./authority/Authority"; +import { Authority, AuthorityType } from "./authority/Authority"; import { ClientInfo } from "./ClientInfo"; import { IdToken } from "./IdToken"; import { Logger } from "./Logger"; @@ -38,6 +38,7 @@ import { Constants, ServerHashParamKeys, InteractionType, libraryVersion, + ResponseTypes, TemporaryCacheKeys, PersistentCacheKeys, ErrorCacheKeys, @@ -66,20 +67,6 @@ declare global { } } -/** - * @hidden - * @ignore - * response_type from OpenIDConnect - * References: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html & https://tools.ietf.org/html/rfc6749#section-4.2.1 - * Since we support only implicit flow in this library, we restrict the response_type support to only 'token' and 'id_token' - * - */ -const ResponseTypes = { - id_token: "id_token", - token: "token", - id_token_token: "id_token token" -}; - /** * @hidden * @ignore @@ -528,8 +515,8 @@ export class UserAgentApplication { // Track the acquireToken progress this.cacheStorage.setItem(TemporaryCacheKeys.INTERACTION_STATUS, Constants.inProgress); - const scope = request.scopes ? request.scopes.join(" ").toLowerCase() : this.clientId.toLowerCase(); - this.logger.verbosePii(`Serialized scopes: ${scope}`); + const requestSignature = request.scopes ? request.scopes.join(" ").toLowerCase() : Constants.oidcScopes.join(" "); + this.logger.verbosePii(`Request signature: ${requestSignature}`); let serverAuthenticationRequest: ServerRequestParameters; const acquireTokenAuthority = (request && request.authority) ? AuthorityFactory.CreateInstance(request.authority, this.config.auth.validateAuthority, request.authorityMetadata) : this.authorityInstance; @@ -545,7 +532,7 @@ export class UserAgentApplication { } // On Fulfillment - const responseType: string = isLoginCall ? ResponseTypes.id_token : this.getTokenType(account, request.scopes, false); + const responseType: string = isLoginCall ? ResponseTypes.id_token : this.getTokenType(account, request.scopes); const loginStartPage = request.redirectStartPage || window.location.href; @@ -569,7 +556,6 @@ export class UserAgentApplication { // Construct urlNavigate const urlNavigate = UrlUtils.createNavigateUrl(serverAuthenticationRequest) + Constants.response_mode_fragment; - // set state in cache if (interactionType === Constants.interactionTypeRedirect) { if (!isLoginCall) { @@ -586,7 +572,7 @@ export class UserAgentApplication { this.logger.verbosePii(`State saved: ${serverAuthenticationRequest.state}`); // Register callback to capture results from server - this.registerCallback(serverAuthenticationRequest.state, scope, resolve, reject); + this.registerCallback(serverAuthenticationRequest.state, requestSignature, resolve, reject); } else { this.logger.verbose("Invalid interaction error. State not cached"); throw ClientAuthError.createInvalidInteractionTypeError(); @@ -689,7 +675,7 @@ export class UserAgentApplication { return this.acquireTokenSilent({ ...request, - scopes: [this.clientId] + scopes: Constants.oidcScopes }); } @@ -742,7 +728,7 @@ export class UserAgentApplication { } // set the response type based on the current cache status / scopes set - const responseType = this.getTokenType(account, request.scopes, true); + const responseType = this.getTokenType(account, request.scopes); this.logger.verbose(`Response type: ${responseType}`); // create a serverAuthenticationRequest populating the `queryParameters` to be sent to the Server @@ -839,12 +825,12 @@ export class UserAgentApplication { this.registerCallback(window.activeRenewals[requestSignature], requestSignature, resolve, reject); } else { - if (request.scopes && request.scopes.indexOf(this.clientId) > -1 && request.scopes.length === 1) { + if (request.scopes && ScopeSet.onlyContainsOidcScopes(request.scopes)) { /* * App uses idToken to send to api endpoints * Default scope is tracked as clientId to store this token */ - this.logger.verbose("ClientId is the only scope, renewing idToken"); + this.logger.verbose("OpenID Connect scopes only, renewing idToken"); this.silentLogin = true; this.renewIdToken(requestSignature, resolve, reject, account, serverAuthenticationRequest); } else { @@ -1614,11 +1600,10 @@ export class UserAgentApplication { * @private */ /* tslint:disable:no-string-literal */ - private saveAccessToken(response: AuthResponse, authority: string, parameters: any, clientInfo: string, idTokenObj: IdToken): AuthResponse { + private saveAccessToken(response: AuthResponse, authority: string, parameters: any, clientInfo: ClientInfo, idTokenObj: IdToken): AuthResponse { this.logger.verbose("SaveAccessToken has been called"); let scope: string; const accessTokenResponse = { ...response }; - const clientObj: ClientInfo = new ClientInfo(clientInfo); let expiration: number; // if the response contains "scope" @@ -1647,8 +1632,8 @@ export class UserAgentApplication { const expiresIn = TimeUtils.parseExpiresIn(parameters[ServerHashParamKeys.EXPIRES_IN]); const parsedState = RequestUtils.parseLibraryState(parameters[ServerHashParamKeys.STATE]); expiration = parsedState.ts + expiresIn; - const accessTokenKey = new AccessTokenKey(authority, this.clientId, scope, clientObj.uid, clientObj.utid); - const accessTokenValue = new AccessTokenValue(parameters[ServerHashParamKeys.ACCESS_TOKEN], idTokenObj.rawIdToken, expiration.toString(), clientInfo); + const accessTokenKey = new AccessTokenKey(authority, this.clientId, scope, clientInfo.uid, clientInfo.utid); + const accessTokenValue = new AccessTokenValue(parameters[ServerHashParamKeys.ACCESS_TOKEN], idTokenObj.rawIdToken, expiration.toString(), clientInfo.encodeClientInfo()); this.cacheStorage.setItem(JSON.stringify(accessTokenKey), JSON.stringify(accessTokenValue)); this.logger.verbose("Saving token to cache"); @@ -1656,18 +1641,17 @@ export class UserAgentApplication { accessTokenResponse.accessToken = parameters[ServerHashParamKeys.ACCESS_TOKEN]; accessTokenResponse.scopes = consentedScopes; } - // if the response does not contain "scope" - scope is usually client_id and the token will be id_token + // if the response does not contain "scope" - scope is set to OIDC scopes by default and the token will be id_token else { this.logger.verbose("Response parameters does not contain scope, clientId set as scope"); - scope = this.clientId; // Generate and cache accessTokenKey and accessTokenValue - const accessTokenKey = new AccessTokenKey(authority, this.clientId, scope, clientObj.uid, clientObj.utid); + const accessTokenKey = new AccessTokenKey(authority, this.clientId, scope, clientInfo.uid, clientInfo.utid); expiration = Number(idTokenObj.expiration); - const accessTokenValue = new AccessTokenValue(parameters[ServerHashParamKeys.ID_TOKEN], parameters[ServerHashParamKeys.ID_TOKEN], expiration.toString(), clientInfo); + const accessTokenValue = new AccessTokenValue(parameters[ServerHashParamKeys.ID_TOKEN], parameters[ServerHashParamKeys.ID_TOKEN], expiration.toString(), clientInfo.encodeClientInfo()); this.cacheStorage.setItem(JSON.stringify(accessTokenKey), JSON.stringify(accessTokenValue)); this.logger.verbose("Saving token to cache"); - accessTokenResponse.scopes = [scope]; + accessTokenResponse.scopes = Constants.oidcScopes; accessTokenResponse.accessToken = parameters[ServerHashParamKeys.ID_TOKEN]; } @@ -1767,7 +1751,7 @@ export class UserAgentApplication { } response.accountState = this.getAccountState(stateInfo.state); - let clientInfo: string = ""; + let clientInfo: ClientInfo; // Process access_token if (hashParams.hasOwnProperty(ServerHashParamKeys.ACCESS_TOKEN)) { @@ -1797,13 +1781,14 @@ export class UserAgentApplication { // retrieve client_info - if it is not found, generate the uid and utid from idToken if (hashParams.hasOwnProperty(ServerHashParamKeys.CLIENT_INFO)) { this.logger.verbose("Fragment has clientInfo"); - clientInfo = hashParams[ServerHashParamKeys.CLIENT_INFO]; + clientInfo = new ClientInfo(hashParams[ServerHashParamKeys.CLIENT_INFO], authority); + } else if (this.authorityInstance.AuthorityType === AuthorityType.Adfs) { + clientInfo = ClientInfo.createClientInfoFromIdToken(idTokenObj, authority); } else { this.logger.warning("ClientInfo not received in the response from AAD"); - throw ClientAuthError.createClientInfoNotPopulatedError("ClientInfo not received in the response from the server"); } - response.account = Account.createAccount(idTokenObj, new ClientInfo(clientInfo)); + response.account = Account.createAccount(idTokenObj, clientInfo); this.logger.verbose("Account object created from response"); let accountKey: string; @@ -1849,18 +1834,20 @@ export class UserAgentApplication { // set the idToken idTokenObj = new IdToken(hashParams[ServerHashParamKeys.ID_TOKEN]); + // set authority + const authority: string = this.populateAuthority(stateInfo.state, this.inCookie, this.cacheStorage, idTokenObj); + response = ResponseUtils.setResponseIdToken(response, idTokenObj); if (hashParams.hasOwnProperty(ServerHashParamKeys.CLIENT_INFO)) { this.logger.verbose("Fragment has clientInfo"); - clientInfo = hashParams[ServerHashParamKeys.CLIENT_INFO]; + clientInfo = new ClientInfo(hashParams[ServerHashParamKeys.CLIENT_INFO], authority); + } else if (this.authorityInstance.AuthorityType === AuthorityType.Adfs) { + clientInfo = ClientInfo.createClientInfoFromIdToken(idTokenObj, authority); } else { this.logger.warning("ClientInfo not received in the response from AAD"); } - // set authority - const authority: string = this.populateAuthority(stateInfo.state, this.inCookie, this.cacheStorage, idTokenObj); - - this.account = Account.createAccount(idTokenObj, new ClientInfo(clientInfo)); + this.account = Account.createAccount(idTokenObj, clientInfo); response.account = this.account; this.logger.verbose("Account object created from response"); @@ -1878,7 +1865,7 @@ export class UserAgentApplication { else { this.logger.verbose("Nonce matches, saving idToken to cache"); this.cacheStorage.setItem(PersistentCacheKeys.IDTOKEN, hashParams[ServerHashParamKeys.ID_TOKEN], this.inCookie); - this.cacheStorage.setItem(PersistentCacheKeys.CLIENT_INFO, clientInfo, this.inCookie); + this.cacheStorage.setItem(PersistentCacheKeys.CLIENT_INFO, clientInfo.encodeClientInfo(), this.inCookie); // Save idToken as access token for app itself this.saveAccessToken(response, authority, hashParams, clientInfo, idTokenObj); @@ -1973,7 +1960,7 @@ export class UserAgentApplication { if (!StringUtils.isEmpty(rawIdToken) && !StringUtils.isEmpty(rawClientInfo)) { const idToken = new IdToken(rawIdToken); - const clientInfo = new ClientInfo(rawClientInfo); + const clientInfo = new ClientInfo(rawClientInfo, ""); this.account = Account.createAccount(idToken, clientInfo); return this.account; } @@ -2009,7 +1996,7 @@ export class UserAgentApplication { for (let i = 0; i < accessTokenCacheItems.length; i++) { const idToken = new IdToken(accessTokenCacheItems[i].value.idToken); - const clientInfo = new ClientInfo(accessTokenCacheItems[i].value.homeAccountIdentifier); + const clientInfo = new ClientInfo(accessTokenCacheItems[i].value.homeAccountIdentifier, ""); const account: Account = Account.createAccount(idToken, clientInfo); accounts.push(account); } @@ -2076,7 +2063,7 @@ export class UserAgentApplication { // Construct AuthenticationRequest based on response type; set "redirectUri" from the "request" which makes this call from Angular - for this.getRedirectUri() const newAuthority = this.authorityInstance ? this.authorityInstance : AuthorityFactory.CreateInstance(this.authority, this.config.auth.validateAuthority); - const responseType = this.getTokenType(accountObject, scopes, true); + const responseType = this.getTokenType(accountObject, scopes); const serverAuthenticationRequest = new ServerRequestParameters( newAuthority, @@ -2262,41 +2249,13 @@ export class UserAgentApplication { * Utils function to create the Authentication * @param {@link account} account object * @param scopes - * @param silentCall * - * @returns {string} token type: id_token or access_token + * @returns {string} token type: token, id_token or id_token token * */ - private getTokenType(accountObject: Account, scopes: string[], silentCall: boolean): string { - /* - * if account is passed and matches the account object/or set to getAccount() from cache - * if client-id is passed as scope, get id_token else token/id_token_token (in case no session exists) - */ - let tokenType: string; - - // acquireTokenSilent - if (silentCall) { - if (Account.compareAccounts(accountObject, this.getAccount())) { - tokenType = (scopes.indexOf(this.config.auth.clientId) > -1) ? ResponseTypes.id_token : ResponseTypes.token; - } - else { - tokenType = (scopes.indexOf(this.config.auth.clientId) > -1) ? ResponseTypes.id_token : ResponseTypes.id_token_token; - } - - return tokenType; - } - // all other cases - else { - if (!Account.compareAccounts(accountObject, this.getAccount())) { - tokenType = ResponseTypes.id_token_token; - } - else { - tokenType = (scopes.indexOf(this.clientId) > -1) ? ResponseTypes.id_token : ResponseTypes.token; - } - - return tokenType; - } - + private getTokenType(accountObject: Account, scopes: string[]): string { + const accountsMatch = Account.compareAccounts(accountObject, this.getAccount()); + return ServerRequestParameters.determineResponseType(accountsMatch, scopes); } /** @@ -2387,7 +2346,7 @@ export class UserAgentApplication { private buildIDTokenRequest(request: AuthenticationParameters): AuthenticationParameters { const tokenRequest: AuthenticationParameters = { - scopes: [this.clientId], + scopes: Constants.oidcScopes, authority: this.authority, account: this.getAccount(), extraQueryParameters: request.extraQueryParameters, diff --git a/lib/msal-core/src/authority/Authority.ts b/lib/msal-core/src/authority/Authority.ts index b3314cdad6..3ab1b63484 100644 --- a/lib/msal-core/src/authority/Authority.ts +++ b/lib/msal-core/src/authority/Authority.ts @@ -11,7 +11,7 @@ import { UrlUtils } from "../utils/UrlUtils"; import TelemetryManager from "../telemetry/TelemetryManager"; import HttpEvent from "../telemetry/HttpEvent"; import { TrustedAuthority } from "./TrustedAuthority"; -import { NetworkRequestType } from "../utils/Constants"; +import { NetworkRequestType, Constants, WELL_KNOWN_SUFFIX } from "../utils/Constants"; /** * @hidden @@ -33,6 +33,17 @@ export class Authority { this.tenantDiscoveryResponse = authorityMetadata; } + public static isAdfs(authorityUrl: string): boolean { + const components = UrlUtils.GetUrlComponents(authorityUrl); + const pathSegments = components.PathSegments; + + return (pathSegments.length && pathSegments[0].toLowerCase() === Constants.ADFS); + } + + public get AuthorityType(): AuthorityType { + return Authority.isAdfs(this.canonicalAuthority)? AuthorityType.Adfs : AuthorityType.Default; + }; + public IsValidationEnabled: boolean; public get Tenant(): string { @@ -87,7 +98,7 @@ export class Authority { // http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata protected get DefaultOpenIdConfigurationEndpoint(): string { - return `${this.CanonicalAuthority}v2.0/.well-known/openid-configuration`; + return (this.AuthorityType === AuthorityType.Adfs)? `${this.CanonicalAuthority}${WELL_KNOWN_SUFFIX}` : `${this.CanonicalAuthority}v2.0/${WELL_KNOWN_SUFFIX}`; } /** diff --git a/lib/msal-core/src/authority/AuthorityFactory.ts b/lib/msal-core/src/authority/AuthorityFactory.ts index 89780cf606..febbc65d47 100644 --- a/lib/msal-core/src/authority/AuthorityFactory.ts +++ b/lib/msal-core/src/authority/AuthorityFactory.ts @@ -11,8 +11,6 @@ import { StringUtils } from "../utils/StringUtils"; import { ClientConfigurationError } from "../error/ClientConfigurationError"; import { ITenantDiscoveryResponse, OpenIdConfiguration } from "./ITenantDiscoveryResponse"; import TelemetryManager from "../telemetry/TelemetryManager"; -import { Constants } from "../utils/Constants"; -import { UrlUtils } from "../utils/UrlUtils"; export class AuthorityFactory { private static metadataMap = new Map(); @@ -63,15 +61,4 @@ export class AuthorityFactory { return new Authority(authorityUrl, validateAuthority, this.metadataMap.get(authorityUrl)); } - - public static isAdfs(authorityUrl: string): boolean { - const components = UrlUtils.GetUrlComponents(authorityUrl); - const pathSegments = components.PathSegments; - - if (pathSegments.length && pathSegments[0].toLowerCase() === Constants.ADFS) { - return true; - } - - return false; - } } diff --git a/lib/msal-core/src/error/ClientConfigurationError.ts b/lib/msal-core/src/error/ClientConfigurationError.ts index e3e6cf122f..674c4e466f 100644 --- a/lib/msal-core/src/error/ClientConfigurationError.ts +++ b/lib/msal-core/src/error/ClientConfigurationError.ts @@ -43,10 +43,6 @@ export const ClientConfigurationErrorMessage: Record { const url = UrlUtils.GetUrlComponents(uri); // validate trusted host - if (AuthorityFactory.isAdfs(uri)) { + if (Authority.isAdfs(uri)) { /** * returning what was passed because the library needs to work with uris that are non * AAD trusted but passed by users such as B2C or others. diff --git a/lib/msal-core/src/utils/Constants.ts b/lib/msal-core/src/utils/Constants.ts index 457766c443..ee630487aa 100644 --- a/lib/msal-core/src/utils/Constants.ts +++ b/lib/msal-core/src/utils/Constants.ts @@ -50,6 +50,7 @@ export class Constants { static get common(): string { return "common"; } static get openidScope(): string { return "openid"; } static get profileScope(): string { return "profile"; } + static get oidcScopes(): Array { return [this.openidScope, this.profileScope] ;} static get interactionTypeRedirect(): InteractionType { return "redirectInteraction"; } static get interactionTypePopup(): InteractionType { return "popupInteraction"; } @@ -72,6 +73,19 @@ export enum ServerHashParamKeys { CLIENT_INFO = "client_info" }; +/** + * @hidden + * @ignore + * response_type from OpenIDConnect + * References: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html & https://tools.ietf.org/html/rfc6749#section-4.2.1 + * + */ +export const ResponseTypes = { + id_token: "id_token", + token: "token", + id_token_token: "id_token token" +}; + /** * @hidden * CacheKeys for MSAL @@ -104,6 +118,7 @@ export enum ErrorCacheKeys { export const DEFAULT_AUTHORITY: string = "https://login.microsoftonline.com/common/"; export const AAD_INSTANCE_DISCOVERY_ENDPOINT: string = `${DEFAULT_AUTHORITY}/discovery/instance?api-version=1.1&authorization_endpoint=`; +export const WELL_KNOWN_SUFFIX: string = ".well-known/openid-configuration"; /** * @hidden @@ -159,5 +174,5 @@ export const FramePrefix = { * MSAL JS Library Version */ export function libraryVersion(): string { - return "1.3.4"; + return "1.4.0"; } diff --git a/lib/msal-core/src/utils/CryptoUtils.ts b/lib/msal-core/src/utils/CryptoUtils.ts index 0931a7fa5c..421cecce67 100644 --- a/lib/msal-core/src/utils/CryptoUtils.ts +++ b/lib/msal-core/src/utils/CryptoUtils.ts @@ -153,7 +153,7 @@ export class CryptoUtils { let match: Array; // Regex for replacing addition symbol with a space const pl = /\+/g; const search = /([^&=]+)=([^&]*)/g; - const decode = (s: string) => decodeURIComponent(decodeURIComponent(s.replace(pl, " "))); // Some values (e.g. state) may need to be decoded twice + const decode = (s: string) => decodeURIComponent(s.replace(pl, " ")); const obj: {} = {}; match = search.exec(query); while (match) { diff --git a/lib/msal-core/src/utils/RequestUtils.ts b/lib/msal-core/src/utils/RequestUtils.ts index 945f2207cf..3e9b636188 100644 --- a/lib/msal-core/src/utils/RequestUtils.ts +++ b/lib/msal-core/src/utils/RequestUtils.ts @@ -48,6 +48,7 @@ export class RequestUtils { // if extraScopesToConsent is passed in loginCall, append them to the login request; Validate and filter scopes (the validate function will throw if validation fails) scopes = isLoginCall ? ScopeSet.appendScopes(request.scopes, request.extraScopesToConsent) : request.scopes; ScopeSet.validateInputScope(scopes, !isLoginCall, clientId); + scopes = ScopeSet.translateClientIdIfSingleScope(scopes, clientId); // validate prompt parameter this.validatePromptParameter(request.prompt); @@ -57,7 +58,6 @@ export class RequestUtils { // validate claimsRequest this.validateClaimsRequest(request.claimsRequest); - } // validate and generate state and correlationId @@ -71,7 +71,7 @@ export class RequestUtils { state, correlationId }; - + return validatedRequest; } diff --git a/lib/msal-core/src/utils/UrlUtils.ts b/lib/msal-core/src/utils/UrlUtils.ts index 77b44f5ea1..84d4abda63 100644 --- a/lib/msal-core/src/utils/UrlUtils.ts +++ b/lib/msal-core/src/utils/UrlUtils.ts @@ -9,7 +9,6 @@ import { ServerRequestParameters } from "../ServerRequestParameters"; import { ScopeSet } from "../ScopeSet"; import { StringUtils } from "./StringUtils"; import { CryptoUtils } from "./CryptoUtils"; -import { ClientConfigurationError } from "./../error/ClientConfigurationError"; /** * @hidden @@ -39,15 +38,10 @@ export class UrlUtils { * @param scopes */ static createNavigationUrlString(serverRequestParams: ServerRequestParameters): Array { - const scopes = serverRequestParams.scopes; + const scopes = ScopeSet.appendDefaultScopes(serverRequestParams.scopes); - if (scopes.indexOf(serverRequestParams.clientId) === -1) { - scopes.push(serverRequestParams.clientId); - } const str: Array = []; str.push("response_type=" + serverRequestParams.responseType); - - this.translateclientIdUsedInScope(scopes, serverRequestParams.clientId); str.push("scope=" + encodeURIComponent(ScopeSet.parseScope(scopes))); str.push("client_id=" + encodeURIComponent(serverRequestParams.clientId)); str.push("redirect_uri=" + encodeURIComponent(serverRequestParams.redirectUri)); @@ -78,23 +72,6 @@ export class UrlUtils { return str; } - /** - * append the required scopes: https://openid.net/specs/openid-connect-basic-1_0.html#Scopes - * @param scopes - */ - private static translateclientIdUsedInScope(scopes: Array, clientId: string): void { - const clientIdIndex: number = scopes.indexOf(clientId); - if (clientIdIndex >= 0) { - scopes.splice(clientIdIndex, 1); - if (scopes.indexOf("openid") === -1) { - scopes.push("openid"); - } - if (scopes.indexOf("profile") === -1) { - scopes.push("profile"); - } - } - } - /** * Returns current window URL as redirect uri */ diff --git a/lib/msal-core/test/Account.spec.ts b/lib/msal-core/test/Account.spec.ts index ee4a65e939..f81815bc76 100644 --- a/lib/msal-core/test/Account.spec.ts +++ b/lib/msal-core/test/Account.spec.ts @@ -3,49 +3,110 @@ import { expect } from "chai"; import { ClientInfo } from "../src/ClientInfo"; import { IdToken } from "../src/IdToken"; import { Account } from "../src/Account"; -import { TEST_TOKENS, TEST_DATA_CLIENT_INFO } from "./TestConstants"; +import { TEST_TOKENS, TEST_DATA_CLIENT_INFO, TEST_CONFIG } from "./TestConstants"; import { CryptoUtils } from "../src/utils/CryptoUtils"; - describe("Account.ts Class", function() { - const idToken: IdToken = new IdToken(TEST_TOKENS.IDTOKEN_V2); - const clientInfo: ClientInfo = new ClientInfo(TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO); + let idToken: IdToken = new IdToken(TEST_TOKENS.IDTOKEN_V2); + let clientInfo: ClientInfo = new ClientInfo(TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, TEST_CONFIG.validAuthority); + + describe("createAccount", () => { + it("verifies account object is created", () => { + const account = Account.createAccount(idToken, clientInfo); + expect(account instanceof Account).to.be.true; + }); + + it("verifies homeAccountIdentifier matches", () => { + const account = Account.createAccount(idToken, clientInfo); + const homeAccountIdentifier = CryptoUtils.base64Encode(TEST_DATA_CLIENT_INFO.TEST_UID) + "." + CryptoUtils.base64Encode(TEST_DATA_CLIENT_INFO.TEST_UTID); + + expect(account.homeAccountIdentifier).to.equal(homeAccountIdentifier); + }); + + it("verifies Account object created matches the idToken parameters", () => { + const account = Account.createAccount(idToken, clientInfo); + + expect(account.accountIdentifier).to.equal(idToken.objectId); + expect(account.userName).to.equal(idToken.preferredName); + expect(account.name).to.equal(idToken.name); + // This will be deprecated soon + expect(account.idToken).to.equal(idToken.claims); + expect(account.idTokenClaims).to.equal(idToken.claims); + expect(account.sid).to.equal(idToken.sid); + expect(account.environment).to.equal(idToken.issuer); + }); + + it("verifies accountIdentifier equal subject claim if objectId not present", () => { + const tempIdToken = idToken; + tempIdToken.objectId = ""; + + const account = Account.createAccount(tempIdToken, clientInfo); + expect(account.accountIdentifier).to.equal(tempIdToken.subject); + }); + + it("verifies homeAccountIdentifier is undefined if ClientInfo is empty", () => { + const tempIdToken = idToken; + tempIdToken.subject = ""; + + const emptyClientInfo = ClientInfo.createClientInfoFromIdToken(tempIdToken, TEST_CONFIG.validAuthority); + const account = Account.createAccount(idToken, emptyClientInfo); + + expect(account.homeAccountIdentifier).to.be.undefined; + }); + }); + describe("compareAccounts", () => { + it("returns false if a1 is null", () => { + const account2 = Account.createAccount(idToken, clientInfo); + expect(Account.compareAccounts(null, account2)).to.be.false; + }); - it("verifies account object is created", function () { + it("returns false if a2 is null", () => { + const account1 = Account.createAccount(idToken, clientInfo); + expect(Account.compareAccounts(account1, null)).to.be.false; + }); - const account = Account.createAccount(idToken, clientInfo); - expect(account instanceof Account).to.be.true; - }); + it("returns false if a1.homeAccountIdentifier evaluates to false", () => { + const tempIdToken = idToken; + tempIdToken.subject = ""; - it("verifies homeAccountIdentifier matches", function () { + const clientInfo2 = ClientInfo.createClientInfoFromIdToken(tempIdToken, TEST_CONFIG.validAuthority); + const account1 = Account.createAccount(idToken, clientInfo2); + const account2 = Account.createAccount(idToken, clientInfo); - const account = Account.createAccount(idToken, clientInfo); - const homeAccountIdentifier = CryptoUtils.base64Encode(TEST_DATA_CLIENT_INFO.TEST_UID) + "." + CryptoUtils.base64Encode(TEST_DATA_CLIENT_INFO.TEST_UTID); + expect(account1.homeAccountIdentifier).to.undefined; + expect(Account.compareAccounts(account1, account2)).to.be.false; + }); - expect(account.homeAccountIdentifier).to.equal(homeAccountIdentifier); - }); + it("returns false if a2.homeAccountIdentifier evaluates to false", () => { + const tempIdToken = idToken; + tempIdToken.subject = ""; - it("verifies Account object created matches the idToken parameters", function () { + const clientInfo2 = ClientInfo.createClientInfoFromIdToken(tempIdToken, TEST_CONFIG.validAuthority); + const account1 = Account.createAccount(idToken, clientInfo); + const account2 = Account.createAccount(idToken, clientInfo2); - const account = Account.createAccount(idToken, clientInfo); + expect(account2.homeAccountIdentifier).to.undefined; + expect(Account.compareAccounts(account1, account2)).to.be.false; + }); - if(idToken.objectId != null) { - expect(account.accountIdentifier).to.equal(idToken.objectId); - } - else { - expect(account.accountIdentifier).to.equal(idToken.subject); - } - - expect(account.userName).to.equal(idToken.preferredName); - expect(account.name).to.equal(idToken.name); - // This will be deprecated soon - expect(account.idToken).to.equal(idToken.claims); - expect(account.idTokenClaims).to.equal(idToken.claims); - expect(account.sid).to.equal(idToken.sid); - expect(account.environment).to.equal(idToken.issuer); - }); + it("returns true if a1.homeAccountIdentifier === a2.homeAccountIdentifier", () => { + const account1 = Account.createAccount(idToken, clientInfo); + const account2 = Account.createAccount(idToken, clientInfo); + + expect(Account.compareAccounts(account1, account2)).to.be.true; + }); + it("returns false if a1.homeAccountIdentifier !== a2.homeAccountIdentifier", () => { + const tempIdToken = idToken; + tempIdToken.subject = "test-oid"; + const clientInfo2 = ClientInfo.createClientInfoFromIdToken(tempIdToken, TEST_CONFIG.validAuthority); + const account1 = Account.createAccount(idToken, clientInfo); + const account2 = Account.createAccount(idToken, clientInfo2); + + expect(Account.compareAccounts(account1, account2)).to.be.false; + }); + }); }); diff --git a/lib/msal-core/test/ClientInfo.spec.ts b/lib/msal-core/test/ClientInfo.spec.ts index be3175c473..a886155eed 100644 --- a/lib/msal-core/test/ClientInfo.spec.ts +++ b/lib/msal-core/test/ClientInfo.spec.ts @@ -3,8 +3,9 @@ import sinon from "sinon"; import { ClientInfo } from "../src/ClientInfo"; import { ClientAuthError, AuthError } from "../src"; import { ClientAuthErrorMessage } from "../src/error/ClientAuthError"; -import { TEST_DATA_CLIENT_INFO } from "./TestConstants"; +import { TEST_DATA_CLIENT_INFO, TEST_CONFIG, TEST_TOKENS } from "./TestConstants"; import { CryptoUtils } from "../src/utils/CryptoUtils"; +import { IdToken } from "../src/IdToken"; describe("Client Info", function () { @@ -12,7 +13,7 @@ describe("Client Info", function () { let clientInfoObj : ClientInfo; beforeEach(function () { - clientInfoObj = new ClientInfo(""); + clientInfoObj = new ClientInfo("", TEST_CONFIG.validAuthority); }); afterEach(function () { @@ -33,6 +34,18 @@ describe("Client Info", function () { }); + describe("createClientInfoFromIdToken", () => { + it("Returns encoded ClientInfo Object", () => { + const tempIdToken: IdToken = new IdToken(TEST_TOKENS.IDTOKEN_V2);; + tempIdToken.subject = "test-oid"; + + const clientInfo = ClientInfo.createClientInfoFromIdToken(tempIdToken, TEST_CONFIG.validAuthority); + + expect(clientInfo.uid).to.equal("test-oid"); + expect(clientInfo.utid).to.equal(""); + }); + }); + describe("Parsing raw client info string", function () { let clientInfoObj : ClientInfo; @@ -43,11 +56,11 @@ describe("Client Info", function () { it("sets uid and utid to empty if null or empty string is passed", function () { let nullString : string = null; - clientInfoObj = new ClientInfo(nullString); + clientInfoObj = new ClientInfo(nullString, TEST_CONFIG.validAuthority); expect(clientInfoObj.uid).to.be.empty; expect(clientInfoObj.utid).to.be.empty; let emptyString = ""; - clientInfoObj = new ClientInfo(emptyString); + clientInfoObj = new ClientInfo(emptyString, TEST_CONFIG.validAuthority); expect(clientInfoObj.uid).to.be.empty; expect(clientInfoObj.utid).to.be.empty; }); @@ -56,7 +69,7 @@ describe("Client Info", function () { let invalidRawString = "youCan'tParseThis"; let authErr : AuthError; try { - clientInfoObj = new ClientInfo(invalidRawString); + clientInfoObj = new ClientInfo(invalidRawString, TEST_CONFIG.validAuthority); } catch (e) { authErr = e; } @@ -74,7 +87,7 @@ describe("Client Info", function () { let authErr : AuthError; try { // What we pass in here doesn't matter since we are stubbing - clientInfoObj = new ClientInfo(TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO); + clientInfoObj = new ClientInfo(TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, TEST_CONFIG.validAuthority); } catch (e) { authErr = e; } @@ -87,15 +100,82 @@ describe("Client Info", function () { expect(authErr.stack).to.include("ClientInfo.spec.ts"); }); - it("correct sets uid and utid if parsing is done correctly", function () { + it("correctly sets uid and utid if parsing is done correctly", function () { + sinon.stub(CryptoUtils, "base64Decode").returns(TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO); + // What we pass in here doesn't matter since we are stubbing + clientInfoObj = new ClientInfo(TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, TEST_CONFIG.validAuthority); + expect(clientInfoObj).to.not.be.null; + expect(clientInfoObj.uid).to.be.eq(TEST_DATA_CLIENT_INFO.TEST_UID); + expect(clientInfoObj.utid).to.be.eq(TEST_DATA_CLIENT_INFO.TEST_UTID); + }); + + it("sets uid and utid if passed authority is null", function () { sinon.stub(CryptoUtils, "base64Decode").returns(TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO); // What we pass in here doesn't matter since we are stubbing - clientInfoObj = new ClientInfo(TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO); + clientInfoObj = new ClientInfo(TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, null); expect(clientInfoObj).to.not.be.null; expect(clientInfoObj.uid).to.be.eq(TEST_DATA_CLIENT_INFO.TEST_UID); expect(clientInfoObj.utid).to.be.eq(TEST_DATA_CLIENT_INFO.TEST_UTID); }); + it("sets uid and utid if passed authority is empty string", function () { + sinon.stub(CryptoUtils, "base64Decode").returns(TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO); + // What we pass in here doesn't matter since we are stubbing + clientInfoObj = new ClientInfo(TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, ""); + expect(clientInfoObj).to.not.be.null; + expect(clientInfoObj.uid).to.be.eq(TEST_DATA_CLIENT_INFO.TEST_UID); + expect(clientInfoObj.utid).to.be.eq(TEST_DATA_CLIENT_INFO.TEST_UTID); + }); + + it("sets uid and utid if passed authority is b2c", function () { + const rawClientInfo = `{"uid":"123-test-uid-testPolicy","utid":"456-test-utid"}`; + sinon.stub(CryptoUtils, "base64Decode").returns(rawClientInfo); + // What we pass in here doesn't matter since we are stubbing + clientInfoObj = new ClientInfo(rawClientInfo, "https://b2cdomain.com/b2ctenant.com/testPolicy"); + expect(clientInfoObj).to.not.be.null; + expect(clientInfoObj.uid).to.be.eq(TEST_DATA_CLIENT_INFO.TEST_UID); + expect(clientInfoObj.utid).to.be.eq(TEST_DATA_CLIENT_INFO.TEST_UTID); + }); + + it("Does not set anything if uid and utid are not part of clientInfo", () => { + sinon.stub(CryptoUtils, "base64Decode").returns(`{"test-uid":"123-test-uid","test-utid":"456-test-utid"}`); + // What we pass in here doesn't matter since we are stubbing + clientInfoObj = new ClientInfo(TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, ""); + expect(clientInfoObj).to.not.be.null; + expect(clientInfoObj.uid).to.be.eq(""); + expect(clientInfoObj.utid).to.be.eq(""); + }); + + it("Does not set utid member if utid not part of ClientInfo", () => { + sinon.stub(CryptoUtils, "base64Decode").returns(`{"uid":"123-test-uid","test-utid":"456-test-utid"}`); + // What we pass in here doesn't matter since we are stubbing + clientInfoObj = new ClientInfo(TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, ""); + expect(clientInfoObj).to.not.be.null; + expect(clientInfoObj.uid).to.be.eq(TEST_DATA_CLIENT_INFO.TEST_UID); + expect(clientInfoObj.utid).to.be.eq(""); + + }); + + it("Does not set uid member if uid not part of ClientInfo", () => { + sinon.stub(CryptoUtils, "base64Decode").returns(`{"test-uid":"123-test-uid","utid":"456-test-utid"}`); + // What we pass in here doesn't matter since we are stubbing + clientInfoObj = new ClientInfo(TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, ""); + expect(clientInfoObj).to.not.be.null; + expect(clientInfoObj.uid).to.be.eq(""); + expect(clientInfoObj.utid).to.be.eq(TEST_DATA_CLIENT_INFO.TEST_UTID); + }); + + describe("stripPolicyFromUid", () => { + it("strips policy from uid", () => { + expect(ClientInfo.stripPolicyFromUid("test-uid-testPolicy", "https://b2cdomain.com/b2ctenant.com/testPolicy")).to.eq("test-uid"); + expect(ClientInfo.stripPolicyFromUid("test-uid-testPolicy", "https://b2cdomain.com/b2ctenant.com/testPolicy/")).to.eq("test-uid"); + }); + + it("returns uid if policy not at end of uid", () => { + expect(ClientInfo.stripPolicyFromUid("test-uid", "https://login.microsoftonline.com/common")).to.eq("test-uid"); + }); + + }); }); }); diff --git a/lib/msal-core/test/ServerRequestParameters.spec.ts b/lib/msal-core/test/ServerRequestParameters.spec.ts index d430b36df8..34e602af34 100644 --- a/lib/msal-core/test/ServerRequestParameters.spec.ts +++ b/lib/msal-core/test/ServerRequestParameters.spec.ts @@ -4,6 +4,7 @@ import { Authority, ClientConfigurationError, Account } from "../src"; import { AuthorityFactory } from "../src/authority/AuthorityFactory"; import { UrlUtils } from "../src/utils/UrlUtils"; import { TEST_CONFIG, TEST_RESPONSE_TYPE, TEST_URIS, TEST_TOKENS, TEST_DATA_CLIENT_INFO } from "./TestConstants"; +import { Constants } from "../src/utils/Constants"; import { ClientConfigurationErrorMessage } from "../src/error/ClientConfigurationError"; import { AuthenticationParameters } from "../src/AuthenticationParameters"; import { RequestUtils } from "../src/utils/RequestUtils"; @@ -33,7 +34,7 @@ describe("ServerRequestParameters.ts Class", function () { expect(scopes.length).to.be.eql(1); }); - it("Scopes are set to client id if null or empty scopes object passed", function () { + it("Scopes are set to OIDC scopes if null or empty scopes object passed", function () { const authority = AuthorityFactory.CreateInstance(TEST_CONFIG.validAuthority, false); sinon.stub(authority, "AuthorizationEndpoint").value(TEST_URIS.TEST_AUTH_ENDPT); const req = new ServerRequestParameters( @@ -45,8 +46,8 @@ describe("ServerRequestParameters.ts Class", function () { TEST_CONFIG.STATE, TEST_CONFIG.CorrelationId ); - expect(req.scopes).to.be.eql([TEST_CONFIG.MSAL_CLIENT_ID]); - expect(req.scopes.length).to.be.eql(1); + expect(req.scopes).to.be.eql(Constants.oidcScopes); + expect(req.scopes.length).to.be.eql(2); }); }); @@ -156,10 +157,10 @@ describe("ServerRequestParameters.ts Class", function () { describe("populateQueryParams", () => { const idToken: IdToken = new IdToken(TEST_TOKENS.IDTOKEN_V2); - const clientInfo: ClientInfo = new ClientInfo(TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO); + const clientInfo: ClientInfo = new ClientInfo(TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, TEST_CONFIG.validAuthority); it("populates parameters", () => { - const serverRequestParameters = new ServerRequestParameters(AuthorityFactory.CreateInstance("https://login.microsoftonline.com/common/", this.validateAuthority), "client-id", "toke", "redirect-uri", [ "user.read" ], "state", "correlationid"); + const serverRequestParameters = new ServerRequestParameters(AuthorityFactory.CreateInstance("https://login.microsoftonline.com/common/", true), "client-id", "toke", "redirect-uri", [ "user.read" ], "state", "correlationid"); serverRequestParameters.populateQueryParams(Account.createAccount(idToken, clientInfo), { scopes: [ "user.read" ], @@ -173,7 +174,7 @@ describe("ServerRequestParameters.ts Class", function () { }); it("populates parameters (null request)", () => { - const serverRequestParameters = new ServerRequestParameters(AuthorityFactory.CreateInstance("https://login.microsoftonline.com/common/", this.validateAuthority), "client-id", "toke", "redirect-uri", [ "user.read" ], "state", "correlationid"); + const serverRequestParameters = new ServerRequestParameters(AuthorityFactory.CreateInstance("https://login.microsoftonline.com/common/", true), "client-id", "toke", "redirect-uri", [ "user.read" ], "state", "correlationid"); serverRequestParameters.populateQueryParams(Account.createAccount(idToken, clientInfo), null); diff --git a/lib/msal-core/test/TestConstants.ts b/lib/msal-core/test/TestConstants.ts index 9a827e911d..0fefa7b3cc 100644 --- a/lib/msal-core/test/TestConstants.ts +++ b/lib/msal-core/test/TestConstants.ts @@ -73,6 +73,10 @@ export const B2C_TEST_CONFIG = { MSAL_CLIENT_ID: "e760cab2-b9a1-4c0d-86fb-ff7084abd902" }; +export const ADFS_TEST_CONFIG = { + validAuthority: "https://fs.msidlab8.com/adfs" +}; + export const TEST_RESPONSE_TYPE = { id_token: "id_token", token: "token", diff --git a/lib/msal-core/test/UserAgentApplication.spec.ts b/lib/msal-core/test/UserAgentApplication.spec.ts index 4988bf0a44..7bb1aae9b4 100644 --- a/lib/msal-core/test/UserAgentApplication.spec.ts +++ b/lib/msal-core/test/UserAgentApplication.spec.ts @@ -242,7 +242,7 @@ describe("UserAgentApplication.ts Class", function () { done(); }); - it("navigates user to redirectURI passed in the request config", (done) => { + it("navigates user to redirectURI passed in the request config", (done) => { window.location = { ...oldWindowLocation, assign: function (url) { @@ -913,24 +913,6 @@ describe("UserAgentApplication.ts Class", function () { done(); }); - it("tests if error is thrown when client id is not passed as single scope", function (done) { - msal.handleRedirectCallback(authCallback); - let authErr: AuthError; - try { - msal.acquireTokenRedirect({ - scopes: [TEST_CONFIG.MSAL_CLIENT_ID, "S1"] - }); - } catch (e) { - authErr = e; - } - expect(authErr.errorCode).to.equal(ClientConfigurationErrorMessage.clientScope.code); - expect(authErr.errorMessage).to.contain(ClientConfigurationErrorMessage.clientScope.desc); - expect(authErr.message).to.contain(ClientConfigurationErrorMessage.clientScope.desc); - expect(authErr.name).to.equal("ClientConfigurationError"); - expect(authErr.stack).to.include("UserAgentApplication.spec.ts"); - done(); - }); - it("throws an error if configured with a null request", () => { let correctError; try { @@ -2324,7 +2306,7 @@ describe("UserAgentApplication.ts Class", function () { const atsStub = sinon.stub(msal, "acquireTokenSilent").callsFake(async (request) => { expect(request.loginHint).to.equal(loginHint); - expect(request.scopes).to.deep.equal([ msal.getCurrentConfiguration().auth.clientId ]); + expect(request.scopes).to.deep.equal(Constants.oidcScopes); atsStub.restore(); done(); @@ -2340,7 +2322,7 @@ describe("UserAgentApplication.ts Class", function () { const atsStub = sinon.stub(msal, "acquireTokenSilent").callsFake(async (request) => { expect(request.sid).to.equal(sid); - expect(request.scopes).to.deep.equal([ msal.getCurrentConfiguration().auth.clientId ]); + expect(request.scopes).to.deep.equal(Constants.oidcScopes); atsStub.restore(); done(); @@ -2360,4 +2342,587 @@ describe("UserAgentApplication.ts Class", function () { } }); }); + + describe("Response type configuration", () => { + // Trailing ampersand is added to avoid id_token matching id_token token + const idTokenType = "response_type=id_token&"; + const idTokenTokenType = "response_type=id_token token&"; + const tokenType = "response_type=token&"; + + describe("Login APIs", () => { + describe("loginRedirect", () => { + beforeEach(function() { + cacheStorage = new AuthCache(TEST_CONFIG.MSAL_CLIENT_ID, "sessionStorage", true); + const config: Configuration = { + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + redirectUri: TEST_URIS.TEST_REDIR_URI + } + }; + msal = new UserAgentApplication(config); + setAuthInstanceStubs(); + setTestCacheItems(); + + delete window.location; + }); + + afterEach(function () { + cacheStorage.clear(); + sinon.restore(); + window.location = oldWindowLocation; + }); + + // Redirect + + it("loginRedirect should set response_type to id_token", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + msal.loginRedirect(); + }); + }); + + describe("loginPopup", () => { + const oldWindow = window; + const TEST_LIBRARY_STATE_POPUP = RequestUtils.generateLibraryState(Constants.interactionTypePopup) + + beforeEach(function() { + cacheStorage = new AuthCache(TEST_CONFIG.MSAL_CLIENT_ID, "sessionStorage", true); + const config: Configuration = { + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + redirectUri: TEST_URIS.TEST_REDIR_URI + } + }; + msal = new UserAgentApplication(config); + setAuthInstanceStubs(); + setTestCacheItems(); + + delete window.location; + }); + + afterEach(function() { + window = oldWindow; + window.location = oldWindowLocation; + cacheStorage.clear(); + sinon.restore(); + }); + + it("loginPopup should set response_type to id_token", (done) => { + let navigateUrl; + window = { + ...oldWindow, + location: { + ...oldWindowLocation, + hash: testHashesForState(TEST_LIBRARY_STATE_POPUP).TEST_SUCCESS_ACCESS_TOKEN_HASH + TEST_USER_STATE_NUM, + }, + open: function (url?, target?, features?, replace?): Window { + navigateUrl = url; + return null; + } + }; + const loginPopupPromise = msal.loginPopup({}); + loginPopupPromise.catch(error => { + expect(navigateUrl).to.include(idTokenType); + expect(navigateUrl).to.not.include(tokenType) + expect(navigateUrl).to.not.include(idTokenTokenType); + done(); + }); + }); + }); + }); + + describe("Acquire Token APIs", () => { + describe("with matching accounts", () => { + beforeEach(function() { + cacheStorage = new AuthCache(TEST_CONFIG.MSAL_CLIENT_ID, "sessionStorage", true); + const config: Configuration = { + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + redirectUri: TEST_URIS.TEST_REDIR_URI, + } + }; + msal = new UserAgentApplication(config); + setAuthInstanceStubs(); + setTestCacheItems(); + + delete window.location; + }); + + afterEach(function () { + cacheStorage.clear(); + sinon.restore(); + window.location = oldWindowLocation; + }); + + it("should set response_type to id_token when clientId is the only input scope", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + sinon.stub(msal, "getAccount").returns(account); + msal.acquireTokenRedirect({ scopes: [TEST_CONFIG.MSAL_CLIENT_ID ], account}); + }); + + it("should set response_type to id_token when openid and profile are the only scopes", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + sinon.stub(msal, "getAccount").returns(account); + msal.acquireTokenRedirect({ scopes: [Constants.openidScope, Constants.profileScope], account}); + }); + + it("should set response_type to id_token when openid is the only scope", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + sinon.stub(msal, "getAccount").returns(account); + msal.acquireTokenRedirect({ scopes: [Constants.openidScope], account}); + }); + + it("should set response_type to id_token when profile is the only scope", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + sinon.stub(msal, "getAccount").returns(account); + msal.acquireTokenRedirect({ scopes: [Constants.profileScope], account}); + }); + + it("should set response_type to id_token when profile is the only scope", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + sinon.stub(msal, "getAccount").returns(account); + msal.acquireTokenRedirect({ scopes: [Constants.profileScope], account}); + }); + + it("should set response_type to id_token token when a resource scope is included along with openid", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + sinon.stub(msal, "getAccount").returns(account); + msal.acquireTokenRedirect({ scopes: ['S1', Constants.openidScope], account}); + }); + + it("should set response_type to id_token token when a resource scope is included along with profile", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + sinon.stub(msal, "getAccount").returns(account); + msal.acquireTokenRedirect({ scopes: ['S1', Constants.profileScope], account}); + }); + + it("should set response_type to id_token token when a resource scope is included along with both OIDC scopes", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + const oidcScopes = [Constants.openidScope, Constants.profileScope]; + sinon.stub(msal, "getAccount").returns(account); + msal.acquireTokenRedirect({ scopes: ['S1', ...oidcScopes], account}); + }); + + it("should treat clientId as a resource scope when included with OIDC scopes and therefore set response_type to id_token token", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + const oidcScopes = [Constants.openidScope, Constants.profileScope]; + sinon.stub(msal, "getAccount").returns(account); + msal.acquireTokenRedirect({ scopes: [TEST_CONFIG.MSAL_CLIENT_ID, ...oidcScopes], account}); + }); + + it("should set response_type to token when only a single resource scope is included", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(tokenType); + expect(url).to.not.include(idTokenTokenType) + expect(url).to.not.include(idTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + const oidcScopes = [Constants.openidScope, Constants.profileScope]; + sinon.stub(msal, "getAccount").returns(account); + msal.acquireTokenRedirect({ scopes: ['S1'], account}); + }); + + it("should set response_type to token when multiple resource scopes are included", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(tokenType); + expect(url).to.not.include(idTokenTokenType) + expect(url).to.not.include(idTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + const oidcScopes = [Constants.openidScope, Constants.profileScope]; + sinon.stub(msal, "getAccount").returns(account); + msal.acquireTokenRedirect({ scopes: ['S1', 'S2'], account}); + }); + it("should treat clientId as a resource scope when included with resource scopes and therefore set response_type to token", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(tokenType); + expect(url).to.not.include(idTokenTokenType) + expect(url).to.not.include(idTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + const oidcScopes = [Constants.openidScope, Constants.profileScope]; + sinon.stub(msal, "getAccount").returns(account); + msal.acquireTokenRedirect({ scopes: [TEST_CONFIG.MSAL_CLIENT_ID, 'S1'], account}); + }); + }); + + describe("when accounts don't match", () => { + beforeEach(function() { + cacheStorage = new AuthCache(TEST_CONFIG.MSAL_CLIENT_ID, "sessionStorage", true); + const config: Configuration = { + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + redirectUri: TEST_URIS.TEST_REDIR_URI, + } + }; + msal = new UserAgentApplication(config); + setAuthInstanceStubs(); + setTestCacheItems(); + + delete window.location; + }); + + afterEach(function () { + cacheStorage.clear(); + sinon.restore(); + window.location = oldWindowLocation; + }); + + it("should set response_type to id_token when clientId is the only input scope", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + msal.acquireTokenRedirect({ scopes: [TEST_CONFIG.MSAL_CLIENT_ID ], account}); + }); + + it("should set response_type to id_token when openid and profile are the only scopes", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + msal.acquireTokenRedirect({ scopes: [Constants.openidScope, Constants.profileScope], account}); + }); + + it("should set response_type to id_token when openid is the only scope", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + msal.acquireTokenRedirect({ scopes: [Constants.openidScope], account }); + }); + + it("should set response_type to id_token when profile is the only scope", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + msal.acquireTokenRedirect({ scopes: [Constants.profileScope], account}); + }); + + it("should set response_type to id_token token when a resource scope is included along with openid", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + msal.acquireTokenRedirect({ scopes: ['S1', Constants.openidScope], account}); + }); + + it("should set response_type to id_token token when a resource scope is included along with profile", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + msal.acquireTokenRedirect({ scopes: ['S1', Constants.profileScope], account}); + }); + + it("should set response_type to id_token token when a resource scope is included along with both OIDC scopes", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + const oidcScopes = [Constants.openidScope, Constants.profileScope]; + msal.acquireTokenRedirect({ scopes: ['S1', ...oidcScopes], account}); + }); + + it("should treat clientId as a resource scope when included with OIDC scopes and therefore set response_type to id_token token", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.include(idTokenTokenType); + expect(url).to.not.include(tokenType) + expect(url).to.not.include(idTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + const oidcScopes = [Constants.openidScope, Constants.profileScope]; + msal.acquireTokenRedirect({ scopes: [TEST_CONFIG.MSAL_CLIENT_ID, ...oidcScopes], account}); + }); + + it("should set response_type to id_token token when only a single resource scope is included because login is required", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.not.include(tokenType); + expect(url).to.include(idTokenTokenType); + expect(url).to.not.include(idTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + const oidcScopes = [Constants.openidScope, Constants.profileScope]; + msal.acquireTokenRedirect({ scopes: ['S1'], account}); + }); + + it("should set response_type to id_token token when multiple resource scopes are included because login is required", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.not.include(tokenType); + expect(url).to.include(idTokenTokenType); + expect(url).to.not.include(idTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + const oidcScopes = [Constants.openidScope, Constants.profileScope]; + msal.acquireTokenRedirect({ scopes: ['S1', 'S2'], account}); + }); + it("should treat clientId as a resource scope when included with resource scopes and therefore set response_type to id_token token because login is required", (done) => { + window.location = { + ...oldWindowLocation, + assign: function (url) { + try { + expect(url).to.not.include(tokenType); + expect(url).to.include(idTokenTokenType) + expect(url).to.not.include(idTokenType); + done(); + } catch (e) { + console.error(e); + } + } + }; + + const oidcScopes = [Constants.openidScope, Constants.profileScope]; + msal.acquireTokenRedirect({ scopes: [TEST_CONFIG.MSAL_CLIENT_ID, 'S1'], account}); + }); + }); + }); + }); }); diff --git a/lib/msal-core/test/authority/Authority.spec.ts b/lib/msal-core/test/authority/Authority.spec.ts index 300159e57c..972e8ab827 100644 --- a/lib/msal-core/test/authority/Authority.spec.ts +++ b/lib/msal-core/test/authority/Authority.spec.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; -import { Authority } from "../../src/authority/Authority"; +import { Authority, AuthorityType } from "../../src/authority/Authority"; import { ClientConfigurationErrorMessage, ClientConfigurationError } from "../../src/error/ClientConfigurationError" -import { TEST_CONFIG, TENANT_DISCOVERY_RESPONSE } from "../TestConstants"; +import { TEST_CONFIG, TENANT_DISCOVERY_RESPONSE, ADFS_TEST_CONFIG } from "../TestConstants"; import TelemetryManager from "../../src/telemetry/TelemetryManager"; import { TelemetryConfig } from "../../src/telemetry/TelemetryTypes"; import { Logger } from "../../src"; @@ -63,6 +63,20 @@ describe("Authority.ts Class", function () { }); }); + describe("get AuthorityType", () => { + it("Default type", () => { + authority = new Authority(TEST_CONFIG.validAuthority, true); + + expect(authority.AuthorityType).to.equal(AuthorityType.Default) + }); + + it("ADFS type", () => { + authority = new Authority(ADFS_TEST_CONFIG.validAuthority, true); + + expect(authority.AuthorityType).to.equal(AuthorityType.Adfs) + }); + }); + describe("get AuthoritzationEndpoint", () => { it("throws error if ResolveEndpointsAsync hasn't been called yet", function () { try { @@ -123,6 +137,16 @@ describe("Authority.ts Class", function () { expect(endpoints.Issuer).to.not.be.undefined; }); + it("returns authority metadata with validateAuthority false", async function () { + authority = new Authority(TEST_CONFIG.validAuthority, false); + sinon.stub(TrustedAuthority, "getTrustedHostList").throws("Validation is disabled. This function should not be called."); + const endpoints = await authority.resolveEndpointsAsync(stubbedTelemetryManager, TEST_CONFIG.CorrelationId); + + expect(endpoints.EndSessionEndpoint).to.not.be.undefined; + expect(endpoints.AuthorizationEndpoint).to.not.be.undefined; + expect(endpoints.Issuer).to.not.be.undefined; + }); + it("Calls Instance Discovery Endpoint if TrustedHostList not set", async function () { // Testing of setTrustedAuthoritiesFromNetwork done in another test let setFromNetworkCalled = false; @@ -164,19 +188,24 @@ describe("Authority.ts Class", function () { }); describe("GetOpenIdConfigurationEndpoint", () => { - it("returns well-known endpoint", async function () { + it("returns well-known endpoint", () => { const endpoint = authority.GetOpenIdConfigurationEndpoint(); - expect(endpoint).to.include("/v2.0/.well-known/openid-configuration"); - expect(endpoint).to.include(TEST_CONFIG.validAuthority); + expect(endpoint).to.equal(TEST_CONFIG.validAuthority + "/v2.0/.well-known/openid-configuration"); }); - it("returns well-known endpoint, alternate authority", async function () { + it("returns well-known endpoint, alternate authority", () => { authority = new Authority(TEST_CONFIG.alternateValidAuthority, true); const endpoint = authority.GetOpenIdConfigurationEndpoint(); - expect(endpoint).to.include("/v2.0/.well-known/openid-configuration"); - expect(endpoint).to.include(TEST_CONFIG.alternateValidAuthority); + expect(endpoint).to.equal(TEST_CONFIG.alternateValidAuthority + "/v2.0/.well-known/openid-configuration"); + }); + + it("returns v1 well-known endpoint, ADFS scenario", () => { + authority = new Authority(ADFS_TEST_CONFIG.validAuthority, true); + const endpoint = authority.GetOpenIdConfigurationEndpoint(); + + expect(endpoint).to.equal(ADFS_TEST_CONFIG.validAuthority + "/.well-known/openid-configuration"); }); }); }); diff --git a/lib/msal-core/test/error/ClientConfigurationError.spec.ts b/lib/msal-core/test/error/ClientConfigurationError.spec.ts index 2fe6f1d493..ffb242593d 100644 --- a/lib/msal-core/test/error/ClientConfigurationError.spec.ts +++ b/lib/msal-core/test/error/ClientConfigurationError.spec.ts @@ -126,27 +126,7 @@ describe("ClientConfigurationError.ts Class", () => { expect(err.name).to.equal("ClientConfigurationError"); expect(err.stack).to.include("ClientConfigurationError.spec.ts"); }); - - it("createClientIdSingleScopeError creates a ClientConfigurationError object", () => { - - const scopesValue = "user.read"; - const singleScopeError = ClientConfigurationError.createClientIdSingleScopeError(scopesValue); - let err: ClientConfigurationError; - - try { - throw singleScopeError; - } catch (error) { - err = error; - } - - expect(err.errorCode).to.equal(ClientConfigurationErrorMessage.clientScope.code); - expect(err.errorMessage).to.include(ClientConfigurationErrorMessage.clientScope.desc); - expect(err.errorMessage).to.include(`Given value: ${scopesValue}`); - expect(err.message).to.include(ClientConfigurationErrorMessage.clientScope.desc); - expect(err.name).to.equal("ClientConfigurationError"); - expect(err.stack).to.include("ClientConfigurationError.spec.ts"); - }); - + it("createScopesRequiredError creates a ClientConfigurationError object", () => { const scopesValue = "random"; diff --git a/lib/msal-core/test/utils/RequestUtils.spec.ts b/lib/msal-core/test/utils/RequestUtils.spec.ts index be18f4fc1e..48a6e43728 100644 --- a/lib/msal-core/test/utils/RequestUtils.spec.ts +++ b/lib/msal-core/test/utils/RequestUtils.spec.ts @@ -46,23 +46,6 @@ describe("RequestUtils.ts class", () => { expect(emptyScopesError.stack).to.include("RequestUtils.spec.ts"); }); - it("ClientId can be sent only as a single scope", () => { - - let improperScopes : ClientConfigurationError; - - try { - const userRequest: AuthenticationParameters = {scopes: [TEST_CONFIG.MSAL_CLIENT_ID, "newScope`"]}; - const request: AuthenticationParameters = RequestUtils.validateRequest(userRequest, false, TEST_CONFIG.MSAL_CLIENT_ID, Constants.interactionTypeSilent); - } catch (e) { - improperScopes = e; - }; - - expect(improperScopes instanceof ClientConfigurationError).to.be.true; - expect(improperScopes.errorCode).to.equal(ClientConfigurationErrorMessage.clientScope.code); - expect(improperScopes.name).to.equal("ClientConfigurationError"); - expect(improperScopes.stack).to.include("RequestUtils.spec.ts"); - }); - it("validate prompt", () => { let promptError: ClientConfigurationError; diff --git a/lib/msal-core/test/utils/UrlUtils.spec.ts b/lib/msal-core/test/utils/UrlUtils.spec.ts index 110b17e474..583ee56762 100644 --- a/lib/msal-core/test/utils/UrlUtils.spec.ts +++ b/lib/msal-core/test/utils/UrlUtils.spec.ts @@ -118,10 +118,9 @@ describe("UrlUtils.ts class", () => { }); describe("deserializeHash", () => { - it("properly decodes a twice encoded value", () => { - // This string is double encoded - // "%257C" = | encoded twice - const hash = "#state=eyJpZCI6IjJkZWQwNGU5LWYzZGYtNGU0Ny04YzRlLWY0MDMyMTU3YmJlOCIsInRzIjoxNTg1OTMyNzg5LCJtZXRob2QiOiJzaWxlbnRJbnRlcmFjdGlvbiJ9%257Chello"; + it("properly decodes an encoded value", () => { + // This string once encoded + const hash = "#state=eyJpZCI6IjJkZWQwNGU5LWYzZGYtNGU0Ny04YzRlLWY0MDMyMTU3YmJlOCIsInRzIjoxNTg1OTMyNzg5LCJtZXRob2QiOiJzaWxlbnRJbnRlcmFjdGlvbiJ9%7Chello"; const { state } = UrlUtils.deserializeHash(hash); @@ -129,6 +128,15 @@ describe("UrlUtils.ts class", () => { expect(stateParts[0]).to.equal("eyJpZCI6IjJkZWQwNGU5LWYzZGYtNGU0Ny04YzRlLWY0MDMyMTU3YmJlOCIsInRzIjoxNTg1OTMyNzg5LCJtZXRob2QiOiJzaWxlbnRJbnRlcmFjdGlvbiJ9"); expect(stateParts[1]).to.equal("hello"); }); + + it("properly decodes a twice encoded value", () => { + // This string is twice encoded + const hash = "#state=eyJpZCI6IjJkZWQwNGU5LWYzZGYtNGU0Ny04YzRlLWY0MDMyMTU3YmJlOCIsInRzIjoxNTg1OTMyNzg5LCJtZXRob2QiOiJzaWxlbnRJbnRlcmFjdGlvbiJ9%257Chello"; + + const { state } = UrlUtils.deserializeHash(hash); + + expect(state).to.equal("eyJpZCI6IjJkZWQwNGU5LWYzZGYtNGU0Ny04YzRlLWY0MDMyMTU3YmJlOCIsInRzIjoxNTg1OTMyNzg5LCJtZXRob2QiOiJzaWxlbnRJbnRlcmFjdGlvbiJ9%7Chello"); + }); }) describe("getUrlComponents", () => { diff --git a/samples/msal-core-samples/VanillaJSTestApp/Readme.md b/samples/msal-core-samples/VanillaJSTestApp/Readme.md index d659d0bda7..2309711962 100644 --- a/samples/msal-core-samples/VanillaJSTestApp/Readme.md +++ b/samples/msal-core-samples/VanillaJSTestApp/Readme.md @@ -1,4 +1,4 @@ -# MSAL.js 1.x Sample - Authorization Code Flow in Single-Page Applications +# MSAL.js 1.x Sample - Implicit Flow in Single-Page Applications ## About this sample This developer sample is used to run basic use cases for the MSAL library. You can also alter the configuration in `./app//authConfig.js` to execute other behaviors. diff --git a/samples/msal-core-samples/VanillaJSTestApp/app/adfs/Readme.md b/samples/msal-core-samples/VanillaJSTestApp/app/adfs/Readme.md new file mode 100644 index 0000000000..9dc9056f60 --- /dev/null +++ b/samples/msal-core-samples/VanillaJSTestApp/app/adfs/Readme.md @@ -0,0 +1,7 @@ +# ADFS Support + +MSAL.js supports connecting to Azure AD, which signs in managed-users (users managed in Azure AD) or federated users (users managed by another identity provider such as ADFS). MSAL.js does not differentiate between these two types of users. As far as it’s concerned, it talks to Azure AD. The authority that you would pass in this case is the normal Azure AD Authority: `https://login.microsoftonline.com/{Enter_the_Tenant_Info_Here}` + +MSAL.js also supports directly connecting to ADFS 2019, which is OpenID Connect compliant and has support for scopes and PKCE. This support requires that a service pack [KB 4490481](https://support.microsoft.com/en-us/help/4490481/windows-10-update-kb4490481) is applied to Windows Server. When connecting directly to ADFS, the authority you'll want to use to build your application will be of form `https://mysite.contoso.com/adfs/` + +Currently, there are no plans to support a direct connection to ADFS 16 or ADFS v2. ADFS 16 does not support scopes, and ADFS v2 is not OIDC compliant. diff --git a/samples/msal-core-samples/VanillaJSTestApp/app/adfs/auth.js b/samples/msal-core-samples/VanillaJSTestApp/app/adfs/auth.js new file mode 100644 index 0000000000..13d6323c85 --- /dev/null +++ b/samples/msal-core-samples/VanillaJSTestApp/app/adfs/auth.js @@ -0,0 +1,93 @@ +// 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; + +// Create the main myMSALObj instance +// configuration parameters are located at authConfig.js +const myMSALObj = new Msal.UserAgentApplication(msalConfig); + +// Register Callbacks for Redirect flow +myMSALObj.handleRedirectCallback(authRedirectCallBack); + +function authRedirectCallBack(error, response) { + if (error) { + console.log(error); + } else { + if (response.tokenType === "id_token" && myMSALObj.getAccount() && !myMSALObj.isCallback(window.location.hash)) { + console.log('id_token acquired at: ' + new Date().toString()); + showWelcomeMessage(myMSALObj.getAccount()); + } else if (response.tokenType === "access_token") { + console.log('access_token acquired at: ' + new Date().toString()); + updateUI(response); + accessTokenButtonPopup.style.display = 'none'; + accessTokenButtonRedirect.style.display = 'none'; + } else { + console.log("token type is:" + response.tokenType); + } + } +} + +// Redirect: once login is successful and redirects with tokens, call Graph API +if (myMSALObj.getAccount() && !myMSALObj.isCallback(window.location.hash)) { + // avoid duplicate code execution on page load in case of iframe and Popup window. + showWelcomeMessage(myMSALObj.getAccount()); +} + +function signIn(method) { + signInType = isIE ? "loginRedirect" : method; + if (signInType === "loginPopup") { + myMSALObj.loginPopup(loginRequest) + .then(loginResponse => { + console.log(loginResponse); + if (myMSALObj.getAccount()) { + showWelcomeMessage(myMSALObj.getAccount()); + } + }).catch(function (error) { + console.log(error); + }); + } else if (signInType === "loginRedirect") { + myMSALObj.loginRedirect(loginRequest) + } +} + +function signOut() { + myMSALObj.logout(); +} + +function getAccessTokenPopup() { + if (myMSALObj.getAccount()) { + myMSALObj.acquireTokenPopup(loginRequest).then(response => { + updateUI(response); + accessTokenButtonPopup.style.display = 'none'; + accessTokenButtonRedirect.style.display = 'none'; + }).catch(error => { + console.log(error); + }); + } +} + +function getAccessTokenRedirect() { + if (myMSALObj.getAccount()) { + myMSALObj.acquireTokenRedirect(loginRequest); + } +} + +function getAccessTokenSilent() { + if (myMSALObj.getAccount()) { + myMSALObj.acquireTokenSilent(loginRequest).then(response => { + updateUI(response); + accessTokenButtonPopup.style.display = 'none'; + accessTokenButtonRedirect.style.display = 'none'; + }).catch(error => { + console.log(error); + }) + } +} \ No newline at end of file diff --git a/samples/msal-core-samples/VanillaJSTestApp/app/adfs/authConfig.js b/samples/msal-core-samples/VanillaJSTestApp/app/adfs/authConfig.js new file mode 100644 index 0000000000..99d20b6c77 --- /dev/null +++ b/samples/msal-core-samples/VanillaJSTestApp/app/adfs/authConfig.js @@ -0,0 +1,18 @@ +// Config object to be passed to Msal on creation +const msalConfig = { + auth: { + clientId: "57448aa1-9515-4176-a106-5cb9be8550e1", + authority: "https://fs.msidlab8.com/adfs/", + knownAuthorities: ["fs.msidlab8.com"] + }, + cache: { + cacheLocation: "localStorage", // This configures where your cache will be stored + storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge + } +}; + +// Add here scopes for id token to be used at MS Identity Platform endpoints. +const loginRequest = { + scopes: ["openid", "profile"], + forceRefresh: false // Set this to "true" to skip a cached token and go to the server to get a new token +}; diff --git a/samples/msal-core-samples/VanillaJSTestApp/app/adfs/index.html b/samples/msal-core-samples/VanillaJSTestApp/app/adfs/index.html new file mode 100644 index 0000000000..abef0cd5fe --- /dev/null +++ b/samples/msal-core-samples/VanillaJSTestApp/app/adfs/index.html @@ -0,0 +1,73 @@ + + + + + + Quickstart | MSAL.JS with ADFS 2019 Vanilla JavaScript SPA + + + + + + + + + + +
+
Vanilla JavaScript SPA calling MS Graph API with MSAL.JS
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/samples/msal-core-samples/VanillaJSTestApp/app/adfs/test/browser.spec.ts b/samples/msal-core-samples/VanillaJSTestApp/app/adfs/test/browser.spec.ts new file mode 100644 index 0000000000..0bb29df6c8 --- /dev/null +++ b/samples/msal-core-samples/VanillaJSTestApp/app/adfs/test/browser.spec.ts @@ -0,0 +1,231 @@ +import * as Mocha from "mocha"; +import puppeteer from "puppeteer"; +import { expect } from "chai"; +import fs from "fs"; +import { LabClient, ILabApiParams } from "../../../e2eTests/LabClient"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots`; +let SCREENSHOT_NUM = 0; +let username = ""; +let accountPwd = ""; + +// Set App Info +const clientId = "57448aa1-9515-4176-a106-5cb9be8550e1"; +const authority = "https://fs.msidlab8.com/adfs/" +const scopes = ["openid"] +const idTokenCacheKey = "msal." + clientId + ".idtoken" +const clientInfoCacheKey = "msal." + clientId + ".client.info" + +function setupScreenshotDir() { + if (!fs.existsSync(`${SCREENSHOT_BASE_FOLDER_NAME}`)) { + fs.mkdirSync(SCREENSHOT_BASE_FOLDER_NAME); + } +} + +async function setupCredentials() { + const testCreds = new LabClient(); + const userParams: ILabApiParams = { + envName: "onprem", + userType: "onprem", + federationProvider: "adfsv2019" + }; + const envResponse = await testCreds.getUserVarsByCloudEnvironment(userParams); + const testEnv = envResponse[0]; + if (testEnv.upn) { + username = testEnv.upn; + } + + const testPwdSecret = await testCreds.getSecret(testEnv.labName); + + accountPwd = testPwdSecret.value; +} + +async function takeScreenshot(page: puppeteer.Page, testName: string, screenshotName: string): Promise { + const screenshotFolderName = `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + if (!fs.existsSync(`${screenshotFolderName}`)) { + fs.mkdirSync(screenshotFolderName); + } + await page.screenshot({ path: `${screenshotFolderName}/${++SCREENSHOT_NUM}_${screenshotName}.png` }); +} + +async function enterCredentials(page: puppeteer.Page, testName: string): Promise { + await takeScreenshot(page, testName, "SignInPage"); + await page.type("#userNameInput", username); + await page.type("#passwordInput", accountPwd); + await page.click("#submitButton"); +} + +async function loginRedirect(page: puppeteer.Page, testName: string): Promise { + // Home Page + await takeScreenshot(page, testName, `samplePageInit`); + // Click Sign In + await page.click("#SignIn"); + await takeScreenshot(page, testName, `signInClicked`); + // Click Sign In With Redirect + await page.click("#loginRedirect"); + await page.waitForSelector("#loginArea"); + // Enter credentials + await enterCredentials(page, testName); + // Wait for return to page + await page.waitForNavigation({ waitUntil: "networkidle0"}); + await page.waitForSelector("#getAccessTokenRedirect"); + await takeScreenshot(page, testName, `samplePageLoggedIn`); +} + +async function loginPopup(page: puppeteer.Page, testName: string): Promise { + // Home Page + await takeScreenshot(page, testName, `samplePageInit`); + // Click Sign In + await page.click("#SignIn"); + await takeScreenshot(page, testName, `signInClicked`); + // Click Sign In With Popup + const newPopupWindowPromise = new Promise(resolve => page.once('popup', resolve)); + await page.click("#loginPopup"); + const popupPage = await newPopupWindowPromise; + const popupWindowClosed = new Promise(resolve => popupPage.once("close", resolve)); + await popupPage.waitForSelector("#loginArea"); + + // Enter credentials + await enterCredentials(popupPage, testName); + // Wait until popup window closes and see that we are logged in + await popupWindowClosed; + await page.waitForSelector("#getAccessTokenPopup"); + await takeScreenshot(page, testName, `samplePageLoggedIn`); +} + +async function validateAccessTokens(page: puppeteer.Page, localStorage: Storage) { + let accessTokensFound = 0 + let accessTokenMatch: boolean; + + Object.keys(localStorage).forEach(async (key) => { + if (key.includes("authority")) { + let cacheKey = JSON.parse(key); + let cachedScopeList = cacheKey.scopes.split(" "); + + accessTokenMatch = cacheKey.authority === authority.toLowerCase() && + cacheKey.clientId.toLowerCase() === clientId.toLowerCase() && + scopes.every(scope => cachedScopeList.includes(scope)); + + if (accessTokenMatch) { + accessTokensFound += 1; + await page.evaluate((key) => window.localStorage.removeItem(key)) + } + } + }); + + return accessTokensFound; +} + +describe("Browser tests", function () { + this.timeout(8000); + this.retries(1); + + let browser: puppeteer.Browser; + before(async () => { + setupScreenshotDir(); + setupCredentials(); + browser = await puppeteer.launch({ + headless: true, + ignoreDefaultArgs: ['--no-sandbox', '–disable-setuid-sandbox'] + }); + }); + + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + + after(async () => { + await context.close(); + await browser.close(); + }); + + describe("Test Login functions", async () => { + beforeEach(async () => { + SCREENSHOT_NUM = 0; + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + await page.goto('http://localhost:30662/'); + }); + + afterEach(async () => { + await page.close(); + }); + + it("Performs loginRedirect", async () => { + const testName = "redirectBaseCase"; + await loginRedirect(page, testName); + + const localStorage = await page.evaluate(() => Object.assign({}, window.localStorage)); + + expect(Object.keys(localStorage)).to.contain(idTokenCacheKey); + expect(Object.keys(localStorage)).to.contain(clientInfoCacheKey); + }); + + it("Performs loginPopup", async () => { + const testName = "popupBaseCase"; + await loginPopup(page, testName); + + const localStorage = await page.evaluate(() => Object.assign({}, window.localStorage)); + expect(Object.keys(localStorage)).to.contain(idTokenCacheKey); + expect(Object.keys(localStorage)).to.contain(clientInfoCacheKey); + }); + }); + + describe("Test AcquireToken functions", async () => { + const testName = "acquireTokenBaseCase"; + + before(async () => { + SCREENSHOT_NUM = 0; + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + await page.goto('http://localhost:30662/'); + await loginPopup(page, testName); + }); + + after(async () => { + await page.close(); + }); + + afterEach(async () => { + await page.reload(); + }); + + it("Test acquireTokenRedirect", async () => { + await page.click("#getAccessTokenRedirect"); + await page.waitForSelector("#access-token-info"); + await takeScreenshot(page, testName, "accessTokenAcquiredRedirect"); + + const localStorage = await page.evaluate(() => Object.assign({}, window.localStorage)); + expect(Object.keys(localStorage)).to.contain(idTokenCacheKey); + expect(Object.keys(localStorage)).to.contain(clientInfoCacheKey); + + const accessTokensFound = await validateAccessTokens(page, localStorage); + expect(accessTokensFound).to.equal(1); + }); + + it("Test acquireTokenPopup", async () => { + await page.click("#getAccessTokenPopup"); + await page.waitForSelector("#access-token-info"); + await takeScreenshot(page, testName, "accessTokenAcquiredPopup"); + + const localStorage = await page.evaluate(() => Object.assign({}, window.localStorage)); + expect(Object.keys(localStorage)).to.contain(idTokenCacheKey); + expect(Object.keys(localStorage)).to.contain(clientInfoCacheKey); + + const accessTokensFound = await validateAccessTokens(page, localStorage); + expect(accessTokensFound).to.equal(1); + }); + + it("Test acquireTokenSilent", async () => { + await page.click("#getAccessTokenSilent"); + await page.waitForSelector("#access-token-info"); + await takeScreenshot(page, testName, "accessTokenAcquiredSilently"); + + const localStorage = await page.evaluate(() => Object.assign({}, window.localStorage)); + expect(Object.keys(localStorage)).to.contain(idTokenCacheKey); + expect(Object.keys(localStorage)).to.contain(clientInfoCacheKey); + + const accessTokensFound = await validateAccessTokens(page, localStorage); + expect(accessTokensFound).to.equal(1); + }); + }); +}); \ No newline at end of file diff --git a/samples/msal-core-samples/VanillaJSTestApp/app/adfs/ui.js b/samples/msal-core-samples/VanillaJSTestApp/app/adfs/ui.js new file mode 100644 index 0000000000..a6af5fa69f --- /dev/null +++ b/samples/msal-core-samples/VanillaJSTestApp/app/adfs/ui.js @@ -0,0 +1,33 @@ +// Select DOM elements to work with +const welcomeDiv = document.getElementById("WelcomeMessage"); +const signInButton = document.getElementById("SignIn"); +const cardDiv = document.getElementById("card-div"); +const accessTokenButtonRedirect = document.getElementById("getAccessTokenRedirect"); +const accessTokenButtonPopup = document.getElementById("getAccessTokenPopup"); +const accessTokenButtonSilent = document.getElementById("getAccessTokenSilent"); +const profileDiv = document.getElementById("profile-div"); + +function showWelcomeMessage(account) { + // Reconfiguring DOM elements + cardDiv.style.display = 'initial'; + welcomeDiv.innerHTML = `Welcome ${account.name}`; + signInButton.nextElementSibling.style.display = 'none'; + signInButton.setAttribute("onclick", "signOut();"); + signInButton.setAttribute('class', "btn btn-success") + signInButton.innerHTML = "Sign Out"; +} + +function updateUI(response) { + const oldAccessTokenDiv = document.getElementById('access-token-info'); + if (oldAccessTokenDiv) { + oldAccessTokenDiv.remove(); + } + const accessTokenDiv = document.createElement('div'); + accessTokenDiv.id = "access-token-info"; + profileDiv.appendChild(accessTokenDiv); + + const scopes = document.createElement('p'); + scopes.innerHTML = "Access Token Acquired for Scopes: " + response.scopes; + + accessTokenDiv.appendChild(scopes); +} \ No newline at end of file diff --git a/samples/msal-core-samples/VanillaJSTestApp/e2eTests/LabClient.ts b/samples/msal-core-samples/VanillaJSTestApp/e2eTests/LabClient.ts index 4baa72b9ae..f59da2ee82 100644 --- a/samples/msal-core-samples/VanillaJSTestApp/e2eTests/LabClient.ts +++ b/samples/msal-core-samples/VanillaJSTestApp/e2eTests/LabClient.ts @@ -5,7 +5,8 @@ const labApiUri = "https://msidlab.com/api" export interface ILabApiParams { envName?: string, userType?: string, - b2cProvider?: string + b2cProvider?: string, + federationProvider?: string }; export class LabClient { @@ -54,6 +55,9 @@ export class LabClient { if (apiParams.b2cProvider) { queryParams.push(`b2cprovider=${apiParams.b2cProvider}`); } + if (apiParams.federationProvider) { + queryParams.push(`federationprovider=${apiParams.federationProvider}`); + } if (queryParams.length <= 0) { throw "Must provide at least one param to getUserVarsByCloudEnvironment";