import * as Msal from "msal" ; import { SignInResult } from "./signin-result" ; import { SignInProvider } from "./signin-provider" ; export class MicrosoftProvider implements SignInProvider { private readonly requestObj : Msal . AuthenticationParameters = { scopes : [ "user.read" ] } ; private readonly graphConfig = { graphMeEndpoint : "https://graph.microsoft.com/v1.0/me" } ; private resolve : ( ( result : SignInResult ) => void ) | null = null ; private reject : ( ( error : any ) => void ) | null = null ; private app : Msal . UserAgentApplication | null = null ; constructor ( private clientId ) { } signIn ( ) { this . resolve = null ; this . reject = null ; return new Promise < SignInResult > ( ( resolve , reject ) => { this . resolve = resolve ; this . reject = reject ; this . signInWithMsal ( ) ; } ) ; } loadDependencies ( ) { return Promise . resolve ( ) ; } private signInWithMsal ( ) { const msalConfig = { auth : { clientId : this . clientId , authority : "https://login.microsoftonline.com/common/" } , cache : { cacheLocation : "localStorage" , storeAuthStateInCookie : true } } ; this . app = new Msal . UserAgentApplication ( msalConfig ) ; this . app . handleRedirectCallback ( ( error , response ) => this . redirectCallback ( error , response ) ) ; this . app . loginPopup ( this . requestObj ) . then ( loginResponse => this . signInSucceeded ( loginResponse ) ) . catch ( error => this . signInFailed ( error ) ) ; } private signInSucceeded ( loginResponse ) { const loginResult = this . getLoginResult ( loginResponse ) ; this . getAccessToken ( loginResponse ) . then ( accessToken => loginResult . providerData ? ( loginResult . providerData [ "accessToken" ] = accessToken ) : accessToken ) . then ( accessToken => this . getUserPhoto ( accessToken ) ) . then ( photoUrl => loginResult . imageUrl = photoUrl ) . catch ( error => console . log ( "Unable to fetch user profile image. Note that Microsoft Graph cannot fetch profile pictures for personal accounts; only work and education accounts are supported. Error details: " , error ) ) . finally ( ( ) => this . resolve ?. ( loginResult ) ) ; } private signInFailed ( error ) { this . reject ?. ( error ) ; } private redirectCallback ( error , response ) { if ( response ) { this . signInSucceeded ( response ) ; } else { this . signInFailed ( error || "Unexpected redirect: no error and no login response" ) ; } } private getAccessToken ( loginResponse ) { if ( ! this . app ) { return Promise . reject ( "No app context" ) ; } return this . app . acquireTokenSilent ( this . requestObj ) . then ( tokenResponse => tokenResponse . accessToken ) ; } private getUserPhoto ( accessToken ) { return this . callGraphApi ( "/photo/$value" , accessToken ) . then ( result => result . blob ( ) ) . then ( blob => this . getImageUrlFromBlob ( blob ) ) ; } private callGraphApi ( relativeUrl , accessToken ) { const url = `${ this . graphConfig . graphMeEndpoint }${ relativeUrl }` ; return fetch ( url , { method : "GET" , headers : new Headers ( { "Authorization" : `Bearer ${ accessToken }` } ) } ) . then ( res => { if ( res . status == 404 ) { return Promise . reject ( `Graph API returned 404 for ${ relativeUrl }` ) ; } return res ; } ) ; } private getImageUrlFromBlob ( blob ) { return new Promise < string > ( ( resolve , reject ) => { const fileReader = new FileReader ( ) ; fileReader . addEventListener ( "error" , error => reject ( error ) ) ; fileReader . addEventListener ( "loadend" , ( ) => resolve ( fileReader . result as string ) ) ; fileReader . readAsDataURL ( blob ) ; } ) ; } private getLoginResult ( loginResponse ) { return { name : loginResponse . account ?. name || "" , email : loginResponse . account ?. userName || "" , provider : "Microsoft" , accessToken : loginResponse . accessToken , accessTokenExpiration : loginResponse . expiresOn , error : null , imageUrl : null , providerData : loginResponse , } ; } }