import { AuthenticationContext , TokenResponse , ErrorResponse , UserCodeInfo , Logging , LoggingLevel } from 'adal-node' ; import { Logger , AccessToken } from '../models' ; import persist from 'node-persist' ; const MS_LOGIN_URL = "https://login.microsoftonline.com" ; export interface Hash < TValue > { [ key ] : TValue ; } class AuthService { connected : boolean = false ; refreshToken ? : string ; accessTokens : Hash < AccessToken > ; tenantId ? : string ; constructor ( ) { this . accessTokens = { } ; } public logout ( ) { this . connected = false ; this . accessTokens = { } ; this . refreshToken = undefined ; this . tenantId = undefined ; } } export class Auth { private authCtx : AuthenticationContext = null ; private service : AuthService = null ; private userCodeInfo ? : UserCodeInfo ; constructor ( private tenantId , private appId , private storage ) { this . service = new AuthService ( ) ; this . authCtx = new AuthenticationContext ( `${ MS_LOGIN_URL }/${ tenantId || "common" }` ) ; } param resource param log param debug param fetchNew public async ensureAccessToken ( resource , log , debug = false , fetchNew = false ) { const accessToken = await this . getToken ( resource ) ; try { const now = new Date ( ) ; const expiresOn = accessToken ? new Date ( accessToken . expiresOn ) : new Date ( 0 ) ; if ( ! fetchNew && accessToken && expiresOn > now ) { if ( debug ) { log . info ( `Existing access token ${ accessToken . value } still valid. Returning...` ) ; } return accessToken . value ; } else { if ( debug ) { if ( ! accessToken ) { log . warn ( `No token found for resource ${ resource }` ) ; } else { log . warn ( `Access token expired. Token: ${ accessToken . value }, ExpiresAt: ${ accessToken . expiresOn }` ) ; } } } let getTokenPromise = this . ensureAccessTokenWithDeviceCode ; if ( accessToken && accessToken . refreshToken ) { if ( debug ) { log . info ( `Retrieving access token with current refresh token: ${ accessToken . refreshToken }` ) ; } this . service . refreshToken = accessToken . refreshToken ; getTokenPromise = this . ensureAccessTokenWithRefreshToken ; } const tokenResponse = await getTokenPromise ( resource , log , debug ) ; if ( ! tokenResponse ) { return null ; } this . service . accessTokens [ resource ] = { expiresOn : tokenResponse . expiresOn as string , value : tokenResponse . accessToken , refreshToken : tokenResponse . refreshToken } ; this . service . refreshToken = tokenResponse . refreshToken ; this . service . connected = true ; await this . setToken ( resource , this . service . accessTokens [ resource ] ) ; await this . storeConnectionInfo ( resource ) ; return this . service . accessTokens [ resource ] . value ; } catch ( error ) { if ( debug ) { log . info ( `Error: ${ error }` ) ; } if ( accessToken ) { await this . removeToken ( resource ) ; } log . error ( `Failed to retrieve an accessToken: ${ error . message }` ) ; this . service = new AuthService ( ) ; return await this . ensureAccessToken ( resource , log , debug , true ) ; } } /**
     * Cancel the device token flow
     */ public cancel ( ) { if ( this . userCodeInfo ) { this . authCtx . cancelRequestToGetTokenWithDeviceCode ( this . userCodeInfo as UserCodeInfo , ( error , response ) : void => { } ) ; } } param resource param log param debug private ensureAccessTokenWithDeviceCode = ( resource , log , debug ) : Promise < TokenResponse > => { if ( debug ) { log . info ( `Starting Auth.ensureAccessTokenWithDeviceCode. resource: ${ resource }, debug: ${ debug }` ) ; } return new Promise < TokenResponse > ( ( resolve , reject ) => { if ( debug ) { log . info ( 'No existing refresh token. Starting new device code flow...' ) ; } this . authCtx . acquireUserCode ( resource , this . appId as string , 'en-us' , ( error , response ) : void => { if ( debug ) { log . info ( 'Response:' ) ; log . info ( response ) ; log . info ( '' ) ; } if ( error ) { reject ( ( response && ( response as any ) . error_description ) || error . message ) ; return ; } log . info ( response . message ) ; this . userCodeInfo = response ; this . authCtx . acquireTokenWithDeviceCode ( resource , this . appId as string , response , ( error , response ) : void => { if ( debug ) { log . info ( 'Response:' ) ; log . info ( response ) ; log . info ( '' ) ; } if ( error || ( response as any ) . error_description ) { reject ( ( response && ( response as any ) . error_description ) || error . message || ( error as any ) . error_description ) ; return ; } this . userCodeInfo = undefined ; resolve ( < TokenResponse > response ) ; } ) ; } ) ; } ) ; } ; param resource param log param debug private ensureAccessTokenWithRefreshToken = ( resource , log , debug ) : Promise < TokenResponse > => { return new Promise < TokenResponse > ( ( resolve , reject ) : void => { if ( debug ) { log . info ( `Retrieving new access token using existing refresh token ${ this . service . refreshToken }` ) ; } this . authCtx . acquireTokenWithRefreshToken ( this . service . refreshToken as string , this . appId as string , resource , ( error , response ) : void => { if ( debug ) { log . info ( 'Response:' ) ; log . info ( response ) ; log . info ( '' ) ; } if ( error || ( response as any ) . error_description ) { reject ( ( response && ( response as any ) . error_description ) || error . message ) ; return ; } resolve ( < TokenResponse > response ) ; } ) ; } ) ; } ; /**
     * Set and get token
     */ private setToken = async ( resource , token ) : Promise < void > => { await this . storage . set ( `${ this . appId }-${ resource }-token` , JSON . stringify ( token ) ) ; } ; private getToken = async ( resource ) : Promise < AccessToken | null > => { const token = await this . storage . get ( `${ this . appId }-${ resource }-token` ) ; if ( token ) { return JSON . parse ( token ) ; } else { return null ; } } ; private removeToken = async ( resource ) : Promise < void > => { await this . storage . del ( `${ this . appId }-${ resource }-token` ) ; } ; /**
     * Store the connection information
     */ private storeConnectionInfo = async ( resource ) : Promise < void > => { await this . storage . set ( `${ this . appId }-${ resource }` , JSON . stringify ( this . service ) ) ; } ; }