|
| 1 | +# Connect client-side web parts to resources secured with Azure Active Directory |
| 2 | + |
| 3 | +When building SharePoint Framework client-side web parts that communicate with resources secured by the Azure Active Directory there are some specific considerations that you must take into account in order to implement the connection to the Microsoft Graph correctly and securely. |
| 4 | + |
| 5 | +## Azure Active Directory for securing Microsoft and organizational resources |
| 6 | + |
| 7 | +Azure Active Directory (AAD) is used to secure Office 365 and its APIs, such as the Microsoft Graph. Organizations can also use AAD to secure their custom applications and APIs. By doing that, they implement security in a consistent way leveraging their organizational accounts and their policies across the whole organization. |
| 8 | + |
| 9 | +## Azure Active Directory authorization flows |
| 10 | + |
| 11 | +Azure Active Directory uses OAuth as the authorization protocol. By completing the OAuth authorization flow applications get a temporary access token that they can use to access the specific resources on behalf of the user using permissions granted them by that user. |
| 12 | + |
| 13 | +Different kinds of applications require a different type of OAuth flow. Web applications use an OAuth flow based on redirects to the URL where the application is hosted which is an additional security measure to verify the authenticity of that application. Client applications on the other hand, not being accessible using a URL, complete the OAuth flow without redirects. Both types of applications use a publicly-know client ID and a privately-held client secret known only to AAD and the application. |
| 14 | + |
| 15 | +Another sort of applications are client-side applications. Implemented using JavaScript these applications are incapable of using a client secret without revealing it to users. For communication with resources secured with AAD these applications can use another type of authorization flow called OAuth implicit flow. In this flow the contract between the application and AAD is established based on the publicly-known client ID and the URL where the application is hosted. This is the flow that SharePoint Framework client-side web parts must use in order to connect to resources secured with AAD. To implement AAD-based authentication and authorization in their applications, developers can use the [ADAL JS library](https://github.com/AzureAD/azure-activedirectory-library-for-js) provided by Microsoft. |
| 16 | + |
| 17 | +## Difference between client-side applications and SharePoint Framework web parts |
| 18 | + |
| 19 | +Typically client-side applications own the whole page and are the only resource available at the given URL. All elements of the page are determined during development and users working with the application cannot dynamically add new elements. Even though a client-side application might consist of a number of views, work with external data and offer a certain degree of configuration, all these aspects are taken into account by developers while building the application. |
| 20 | + |
| 21 | +When working with SharePoint, users compose pages by adding and configuring web parts. A single web part is only a small part of the whole page and developers building that web part cannot know upfront whether there will be other web parts placed on the same page and if so, which resources these web parts use. In contrary to SharePoint add-ins, SharePoint Framework client-side web parts are a part of the page. All web parts have access to the page's DOM and resources loaded by one web part can be available to other web parts. |
| 22 | + |
| 23 | +## Limitations when using ADAL JS with SharePoint Framework client-side web parts |
| 24 | + |
| 25 | +While the ADAL JS library significantly simplifies implementing AAD-based OAuth flow in client-side applications, it has a number of limitations when used with SharePoint Framework client-side web parts. These limitations are mainly caused by the fact that ADAL JS was designed to be used by client-side applications that own the whole page and not share it with other applications. |
| 26 | + |
| 27 | +### Shared storage |
| 28 | + |
| 29 | +Depending on how you configured the ADAL JS library in your project, it will store its data either in the browser's local storage or in the session storage. Either way, the information stored by ADAL JS is accessible to any component on the same page. If one web part for example would retrieve an access token for Microsoft Graph, then theoretically any other web part on the same page would be able to reuse that token and communicate with the Microsoft Graph. |
| 30 | + |
| 31 | +### ADAL JS as a singleton |
| 32 | + |
| 33 | +ADAL JS is designed to work as a singleton service registered in the global scope of the page. When processing OAuth flow callbacks in iframes, ADAL JS code uses the registered singleton reference from the main window to resolve the token flow. Since each web part is registered with a different AAD application, reusing the same instance of ADAL JS leads to undesirable results where every web part uses the same configuration as the one registered by the first web part loaded on the page. |
| 34 | + |
| 35 | +### Resource-based storage |
| 36 | + |
| 37 | +By default the ADAL JS library stores information such as the current access token or its expiration time in a set of keys associated with the particular resource, for example Microsoft Graph or SharePoint. If there are multiple components on one page accessing the same resource with different permissions, then the first retrieved token will be served by ADAL JS to all components. If different components require access to the same resource then they might fail. To illustrate this limitation, consider the following example. |
| 38 | + |
| 39 | +Imagine that you had two web parts on the page: one that would list upcoming meetings and one that would allow you to create a new meeting. To read upcoming meetings, the upcoming meetings web part would use a Microsoft Graph access token with permissions to read user's calendar. The web part that creates new meetings on the other hand, requires a Microsoft Graph access token with permissions to write to user's calendar. If the upcoming meetings web part loaded first and ADAL JS would retrieve its token, the same token would be served by ADAL JS to the web part creating new meetings. As you can predict, trying to create a new meeting the web part would fail with an access denied error. Because of the resource-based storage, ADAL JS wouldn't retrieve a new token for Microsoft Graph until the previously retrieved token expired. |
| 40 | + |
| 41 | +### Processing authorization callbacks |
| 42 | + |
| 43 | +OAuth implicit flow is based on redirects between Azure Active Directory and the application participating in the flow. When the authentication process starts, the application redirects you to the AAD login page. Once the authentication completes, AAD redirects you back to the application sending the identity token in the URL hash for the application to process. From the perspective of a web part placed on a SharePoint page this behavior has two drawbacks. |
| 44 | + |
| 45 | +Despite that you are logged in in SharePoint, you need to separately authenticate with AAD in order for your web part to be able to access resources secured with AAD. Even though the web part is small part of the overal page, in order to complete the authentication process, you would need to leave the whole page which is a poor user experience. |
| 46 | + |
| 47 | +After the authentication completes, Azure AD redirects you back to your application. In the URL hash it includes the identity token that your application needs to process. Unfortunately, as the identity token doesn't contain any information about its origin, if you had multiple web parts on one page, all of them would try to process the identity token from the URL. |
| 48 | + |
| 49 | +## Use ADAL JS with SharePoint Framework client-side web parts |
| 50 | + |
| 51 | +ADAL JS has some limitations when used in SharePoint Framework client-side web part. With little effort you can overcome them and benefit of ADAL JS to connect your web parts to resources secured with AAD. |
| 52 | + |
| 53 | +### Load ADAL JS in your web part |
| 54 | + |
| 55 | +SharePoint Framework web parts are built using TypeScript and distributed as AMD modules. Their modular architecture, allows you to isolate the different resources used by the web part. While ADAL JS has been designed to register itself in the global scope, it can be as well loaded in SharePoint Framework web parts using the following construct: |
| 56 | + |
| 57 | +```ts |
| 58 | +const AuthenticationContext = require('adal-angular'); |
| 59 | +``` |
| 60 | + |
| 61 | +This statement imports the `AuthenticationContext` class that exposes the ADAL JS functionality for use in web parts. Despite the name of the module, the `AuthenticationContext` class works with every JavaScript library. |
| 62 | + |
| 63 | +### Make ADAL JS suitable for use with SharePoint Framework web parts |
| 64 | + |
| 65 | +The way ADAL JS works by default, makes it unsuitable for use in SharePoint Framework web parts. Using the following patch you can change how ADAL JS works and make it work inside web parts: |
| 66 | + |
| 67 | +**WebPartAuthenticationContext.js:** |
| 68 | +```js |
| 69 | +const AuthenticationContext = require('adal-angular'); |
| 70 | + |
| 71 | +AuthenticationContext.prototype._getItemSuper = AuthenticationContext.prototype._getItem; |
| 72 | +AuthenticationContext.prototype._saveItemSuper = AuthenticationContext.prototype._saveItem; |
| 73 | +AuthenticationContext.prototype.handleWindowCallbackSuper = AuthenticationContext.prototype.handleWindowCallback; |
| 74 | +AuthenticationContext.prototype._renewTokenSuper = AuthenticationContext.prototype._renewToken; |
| 75 | +AuthenticationContext.prototype.getRequestInfoSuper = AuthenticationContext.prototype.getRequestInfo; |
| 76 | + |
| 77 | +AuthenticationContext.prototype._getItem = function (key) { |
| 78 | + if (this.config.webPartId) { |
| 79 | + key = this.config.webPartId + '_' + key; |
| 80 | + } |
| 81 | + |
| 82 | + return this._getItemSuper(key); |
| 83 | +}; |
| 84 | + |
| 85 | +AuthenticationContext.prototype._saveItem = function (key, object) { |
| 86 | + if (this.config.webPartId) { |
| 87 | + key = this.config.webPartId + '_' + key; |
| 88 | + } |
| 89 | + |
| 90 | + return this._saveItemSuper(key, object); |
| 91 | +}; |
| 92 | + |
| 93 | +AuthenticationContext.prototype.handleWindowCallback = function (hash) { |
| 94 | + if (hash == null) { |
| 95 | + hash = window.___location.hash; |
| 96 | + } |
| 97 | + |
| 98 | + if (!this.isCallback(hash)) { |
| 99 | + return; |
| 100 | + } |
| 101 | + |
| 102 | + var requestInfo = this.getRequestInfo(hash); |
| 103 | + if (requestInfo.requestType === this.REQUEST_TYPE.LOGIN) { |
| 104 | + return this.handleWindowCallbackSuper(hash); |
| 105 | + } |
| 106 | + |
| 107 | + var resource = this._getResourceFromState(requestInfo.stateResponse); |
| 108 | + if (!resource || resource.length === 0) { |
| 109 | + return; |
| 110 | + } |
| 111 | + |
| 112 | + if (this._getItem(this.CONSTANTS.STORAGE.RENEW_STATUS + resource) === this.CONSTANTS.TOKEN_RENEW_STATUS_IN_PROGRESS) { |
| 113 | + return this.handleWindowCallbackSuper(hash); |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +AuthenticationContext.prototype._renewToken = function (resource, callback) { |
| 118 | + this._renewTokenSuper(resource, callback); |
| 119 | + var _renewStates = this._getItem('renewStates'); |
| 120 | + if (_renewStates) { |
| 121 | + _renewStates = _renewStates.split(';'); |
| 122 | + } |
| 123 | + else { |
| 124 | + _renewStates = []; |
| 125 | + } |
| 126 | + _renewStates.push(this.config.state); |
| 127 | + this._saveItem('renewStates', _renewStates); |
| 128 | +} |
| 129 | + |
| 130 | +AuthenticationContext.prototype.getRequestInfo = function (hash) { |
| 131 | + var requestInfo = this.getRequestInfoSuper(hash); |
| 132 | + var _renewStates = this._getItem('renewStates'); |
| 133 | + if (!_renewStates) { |
| 134 | + return requestInfo; |
| 135 | + } |
| 136 | + |
| 137 | + _renewStates = _renewStates.split(';'); |
| 138 | + for (var i = 0; i < _renewStates.length; i++) { |
| 139 | + if (_renewStates[i] === requestInfo.stateResponse) { |
| 140 | + requestInfo.requestType = this.REQUEST_TYPE.RENEW_TOKEN; |
| 141 | + requestInfo.stateMatch = true; |
| 142 | + break; |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + return requestInfo; |
| 147 | +} |
| 148 | + |
| 149 | +window.AuthenticationContext = function() { |
| 150 | + return undefined; |
| 151 | +} |
| 152 | +``` |
| 153 | + |
| 154 | +The patch applies the following changes on top of ADAL JS: |
| 155 | +- all information is stored in web part-specific keys (override of the `_getItem` and `_saveItem` functions) |
| 156 | +- callbacks are processed only by web parts that initiated them (override of the `handleWindowCallback` function) |
| 157 | +- when verifying data from callbacks, the instance of the `AuthenticationContext` class of the specific web part is used instead of the globally registered singleton (`_renewToken`, `getRequestInfo` and the empty registration of the `window.AuthenticationContext` function) |
| 158 | + |
| 159 | +### Use the ADAL JS patch in SharePoint Framework web parts |
| 160 | + |
| 161 | +For ADAL JS to work correctly in SharePoint Framework web parts, you have to configure it in a specific way. To do that, you first need to define a custom interface that extends the standard ADAL JS `Config` interface to expose additional properties on the configuration object: |
| 162 | + |
| 163 | +```ts |
| 164 | +export interface IAdalConfig extends adal.Config { |
| 165 | + popUp?: boolean; |
| 166 | + callback?: (error: any, token: string) => void; |
| 167 | + webPartId?: string; |
| 168 | +} |
| 169 | +``` |
| 170 | + |
| 171 | +The `popUp` property and the `callback` function are both already implemented in ADAL JS but are not exposed in the TypeScript typings of the standard Config interface. You need them in order to allow users to sign in to your web parts using a pop-up window instead of redirecting to the AAD login page. |
| 172 | + |
| 173 | +Next, you load the ADAL JS patch, the custom configuration interface and your configuration object into the main component of your web part: |
| 174 | + |
| 175 | +```tsx |
| 176 | +const AuthenticationContext = require('adal-angular'); |
| 177 | +import adalConfig from '../AdalConfig'; |
| 178 | +import { IAdalConfig } from '../../IAdalConfig'; |
| 179 | +import '../../WebPartAuthenticationContext'; |
| 180 | +``` |
| 181 | + |
| 182 | +In the constructor of your component, you extend the ADAL JS configuration with additional properties and use it to create a new instance of the `AuthenticationContext` class: |
| 183 | + |
| 184 | +```tsx |
| 185 | +export default class UpcomingMeetings extends React.Component<IUpcomingMeetingsProps, IUpcomingMeetingsState> { |
| 186 | + private authCtx: adal.AuthenticationContext; |
| 187 | + |
| 188 | + constructor(props: IUpcomingMeetingsProps, state: IUpcomingMeetingsState) { |
| 189 | + super(props); |
| 190 | + |
| 191 | + this.state = { |
| 192 | + loading: false, |
| 193 | + error: null, |
| 194 | + upcomingMeetings: [], |
| 195 | + signedIn: false |
| 196 | + }; |
| 197 | + |
| 198 | + const config: IAdalConfig = adalConfig; |
| 199 | + config.webPartId = this.props.webPartId; |
| 200 | + config.popUp = true; |
| 201 | + config.callback = (error: any, token: string): void => { |
| 202 | + this.setState((previousState: IUpcomingMeetingsState, currentProps: IUpcomingMeetingsProps): IUpcomingMeetingsState => { |
| 203 | + previousState.error = error; |
| 204 | + previousState.signedIn = !(!this.authCtx.getCachedUser()); |
| 205 | + return previousState; |
| 206 | + }); |
| 207 | + }; |
| 208 | + |
| 209 | + this.authCtx = new AuthenticationContext(config); |
| 210 | + AuthenticationContext.prototype._singletonInstance = undefined; |
| 211 | + } |
| 212 | +} |
| 213 | +``` |
| 214 | + |
| 215 | +You start with retrieving the standard ADAL JS configuration object and casting it to the type of the newly defined configuration interface. Then, you pass the ID of the web part instance so that all ADAL JS values can be stored in a way that won't collide with other web parts on the page. Next, you enable the pop-up-based authentication and define a callback function that should execute when authentication completes to update the component. Finally, you create a new instance of the `AuthenticationContext` class and reset the `_singletonInstance` set in the constructor of the `AuthenticationContext` class. This is required in order to allow every web part use its own version of the ADAL JS authentication context. |
| 216 | + |
| 217 | +## Considerations when using OAuth implicit flow in SharePoint Framework client-side web parts |
| 218 | + |
| 219 | +Before you decide to build SharePoint Framework client-side web parts that communicate with AAD-secured resources, there are some things that you should consider. This will allow you to verify that how OAuth authorization in web parts works is acceptable for your organization. |
| 220 | + |
| 221 | +### SharePoint Framework web parts are highly trusted |
| 222 | + |
| 223 | +Unlike SharePoint add-ins, SharePoint Framework client-side web parts execute with the permissions of the current user. Whatever the user can do, the SharePoint Framework web part can do as well, given that there is an API for, that can be used by the web part. This is exactly why SharePoint Framework web parts can be installed and deployed only by tenant administrators, who should verify, that web parts that they are deploying, come from a trusted source and have been approved for use in the particular tenant. |
| 224 | + |
| 225 | +SharePoint Framework client-side web parts are a part of the page and, unlike SharePoint add-ins, share DOM and resources with other elements on the page. Access tokens, that grant access to resources secured with AAD, are retrieved through a callback to the same page where the web part is located. That callback can be processed by any element on the page. Also, once access tokens are processed from callbacks, they are stored in the browser's local storage or session storage from where they can be retrieved by any component on the page. A malicious SharePoint Framework client-side web part could read the token and either expose the token or the data it retrieved using that token to an external service. |
| 226 | + |
| 227 | +### URL is a part of the OAuth implicit flow contract |
| 228 | + |
| 229 | +Client-side applications are incapable of storing a client secret without revealing it to users. In order to verify the authenticity of the particular application, the URL where the application is hosted, is used as a part of the trust contract between AAD and the application. The consequence of this is, that the URL of every page with web parts using OAuth implicit flow must be registered with Azure Active Directory. Without it, the OAuth flow will fail and the web part will be denied the access to the secured resource. |
| 230 | + |
| 231 | +This necessary security measure is a significant limitation for organizations that allow users to add web parts to pages. The recommendation is, to limit the locations to where web parts using OAuth implicit flow can be used, to a few known locations such as home page or specific landing pages and have these locations registered with Azure Active Directory. |
| 232 | + |
| 233 | +### Regular re-authentication required |
| 234 | + |
| 235 | +When using regular OAuth flow web applications and client applications get a short-lived access token and a refresh token which is valid for a longer period of time. The refresh token can be used to get a new access token once the previously retrieved one expired. Using this approach users don't need to login to AAD frequently and can keep using the application for a longer period of time. |
| 236 | + |
| 237 | +Because client-side applications are incapable of securely storing secrets, and disclosing a refresh token is a serious security threat, they only receive access token as part of the OAuth implicit flow. Once that token expires, the application must start a new OAuth flow which could require the user to re-authenticate. |
0 commit comments