UNPKG

@microsoft/mgt

Version:
337 lines (296 loc) • 9.39 kB
/** * ------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. * See License in the project root for license information. * ------------------------------------------------------------------------------------------- */ import { AuthenticationProviderOptions } from '@microsoft/microsoft-graph-client/lib/es/IAuthenticationProviderOptions'; import { AuthenticationParameters, Configuration, UserAgentApplication } from 'msal'; import { LoginType, ProviderState } from './IProvider'; import { MsalProvider } from './MsalProvider'; // tslint:disable-next-line: completed-docs declare var microsoftTeams: any; // tslint:disable-next-line: completed-docs declare global { // tslint:disable-next-line: completed-docs interface Window { // tslint:disable-next-line: completed-docs nativeInterface: any; } } /** * Interface used to store authentication parameters in session storage * between redirects * * @interface AuthParams */ interface AuthParams { /** * The app clientId * * @type {string} * @memberof AuthParams */ clientId?: string; /** * The comma separated scopes * * @type {string} * @memberof AuthParams */ scopes?: string; /** * The login hint to be used for authentication * * @type {string} * @memberof AuthParams */ loginHint?: string; } /** * Interface to define the configuration when creating a TeamsProvider * * @export * @interface TeamsConfig */ export interface TeamsConfig { /** * The app clientId * * @type {string} * @memberof TeamsConfig */ clientId: string; /** * The relative or absolute path of the html page that will handle the authentication * * @type {string} * @memberof TeamsConfig */ authPopupUrl: string; /** * The scopes to use when authenticating the user * * @type {string[]} * @memberof TeamsConfig */ scopes?: string[]; /** * Additional Msal configurations options to use * See Msal.js documentation for more details * * @type {Configuration} * @memberof TeamsConfig */ msalOptions?: Configuration; } /** * Enables authentication of Single page apps inside of a Microsoft Teams tab * * @export * @class TeamsProvider * @extends {MsalProvider} */ export class TeamsProvider extends MsalProvider { /** * Gets whether the Teams provider can be used in the current context * (Whether the app is running in Microsoft Teams) * * @readonly * @static * @memberof TeamsProvider */ public static get isAvailable() { if (window.parent === window.self && window.nativeInterface) { // In Teams mobile client return true; } else if (window.name === 'embedded-page-container' || window.name === 'extension-tab-frame') { // In Teams web/desktop client return true; } else { return false; } } /** * Optional entry point to the teams library * If this value is not set, the provider will attempt to use * the microsoftTeams global variable. * * @static * @memberof TeamsProvider */ public static microsoftTeamsLib; /** * Handle all authentication redirects in the authentication page and authenticates the user * * @static * @returns * @memberof TeamsProvider */ public static async handleAuth() { // we are in popup world now - authenticate and handle it const teams = TeamsProvider.microsoftTeamsLib || microsoftTeams; if (!teams) { // tslint:disable-next-line: no-console console.error('Make sure you have referenced the Microsoft Teams sdk before using the TeamsProvider'); return; } teams.initialize(); // msal checks for the window.opener.msal to check if this is a popup authentication // and gets a false positive since teams opens a popup for the authentication. // in reality, we are doing a redirect authentication and need to act as if this is the // window initiating the authentication if (window.opener) { window.opener.msal = null; } const url = new URL(window.location.href); const paramsString = sessionStorage.getItem(this._sessionStorageParametersKey); let authParams: AuthParams; if (paramsString) { authParams = JSON.parse(paramsString); } else { authParams = {}; } if (!authParams.clientId) { authParams.clientId = url.searchParams.get('clientId'); authParams.scopes = url.searchParams.get('scopes'); authParams.loginHint = url.searchParams.get('loginHint'); sessionStorage.setItem(this._sessionStorageParametersKey, JSON.stringify(authParams)); } if (!authParams.clientId) { teams.authentication.notifyFailure('no clientId provided'); return; } const scopes = authParams.scopes ? authParams.scopes.split(',') : null; const provider = new MsalProvider({ clientId: authParams.clientId, options: { auth: { clientId: authParams.clientId, redirectUri: url.protocol + '//' + url.host + url.pathname }, system: { loadFrameTimeout: 10000 } }, scopes }); if ((UserAgentApplication.prototype as any).urlContainsHash(window.location.hash)) { // the page should redirect again return; } const handleProviderState = async () => { // how do we handle when user can't sign in // change to promise and return status if (provider.state === ProviderState.SignedOut) { provider.login({ loginHint: authParams.loginHint, scopes: scopes || provider.scopes }); } else if (provider.state === ProviderState.SignedIn) { try { const accessToken = await provider.getAccessTokenForScopes(...provider.scopes); sessionStorage.removeItem(this._sessionStorageParametersKey); teams.authentication.notifySuccess(accessToken); } catch (e) { sessionStorage.removeItem(this._sessionStorageParametersKey); teams.authentication.notifyFailure(e); } } }; provider.onStateChanged(handleProviderState); handleProviderState(); } private static _sessionStorageParametersKey = 'msg-teamsprovider-auth-parameters'; /** * Scopes used for authentication * * @type {string[]} * @memberof TeamsProvider */ public scopes: string[]; private teamsContext; private _authPopupUrl: string; constructor(config: TeamsConfig) { super({ clientId: config.clientId, loginType: LoginType.Redirect, options: config.msalOptions, scopes: config.scopes }); const teams = TeamsProvider.microsoftTeamsLib || microsoftTeams; this._authPopupUrl = config.authPopupUrl; teams.initialize(); } /** * Opens the teams authentication popup to the authentication page * * @returns {Promise<void>} * @memberof TeamsProvider */ public async login(): Promise<void> { this.setState(ProviderState.Loading); const teams = TeamsProvider.microsoftTeamsLib || microsoftTeams; return new Promise((resolve, reject) => { teams.getContext(context => { this.teamsContext = context; const url = new URL(this._authPopupUrl, new URL(window.location.href)); url.searchParams.append('clientId', this.clientId); if (context.loginHint) { url.searchParams.append('loginHint', context.loginHint); } if (this.scopes) { url.searchParams.append('scopes', this.scopes.join(',')); } teams.authentication.authenticate({ failureCallback: reason => { this.setState(ProviderState.SignedOut); reject(); }, successCallback: result => { this.setState(ProviderState.SignedIn); resolve(); }, url: url.href }); }); }); } /** * Returns an access token that can be used for making calls to the Microsoft Graph * * @param {AuthenticationProviderOptions} options * @returns {Promise<string>} * @memberof TeamsProvider */ public async getAccessToken(options: AuthenticationProviderOptions): Promise<string> { if (!this.teamsContext) { const teams = TeamsProvider.microsoftTeamsLib || microsoftTeams; this.teamsContext = await teams.getContext(); } const scopes = options ? options.scopes || this.scopes : this.scopes; const accessTokenRequest: AuthenticationParameters = { scopes }; if (this.teamsContext && this.teamsContext.loginHint) { accessTokenRequest.loginHint = this.teamsContext.loginHint; } const currentParent = window.parent; if (document.referrer.startsWith('https://teams.microsoft.com/')) { (window as any).parent = window; } try { const response = await this._userAgentApplication.acquireTokenSilent(accessTokenRequest); (window as any).parent = currentParent; return response.accessToken; } catch (e) { (window as any).parent = currentParent; if (this.requiresInteraction(e)) { // nothing we can do now until we can do incremental consent return null; } else { throw e; } } } }