Skip to content

Commit

Permalink
Refactor to use an MSAL-specific client
Browse files Browse the repository at this point in the history
  • Loading branch information
Jonathan Turner committed Oct 14, 2020
1 parent 11cdaaa commit e9342e8
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 121 deletions.
182 changes: 182 additions & 0 deletions sdk/identity/identity/src/client/msalClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { CredentialUnavailable } from "./errors";
import {
PublicClientApplication,
Configuration,
AuthorizationCodeRequest,
AuthenticationResult,
} from "@azure/msal-node";
import {IdentityClient, TokenCredentialOptions} from "./identityClient";
import { AccessToken } from "@azure/core-http";
import { credentialLogger } from "../util/logging";

let msalExt: any;
try {
msalExt = require("@azure/msal-node-extension");
} catch (er) {
msalExt = null;
}

const logger = credentialLogger("InteractiveBrowserCredential");

async function createPersistence(cachePath?: string): Promise<
| {
cachePlugin?: {
readFromStorage: () => Promise<string>;
writeToStorage: (getMergedState: (oldState: string) => string) => Promise<void>;
};
}
| undefined
> {
console.log("process platform:", process.platform);

// On Windows, uses a DPAPI encrypted file
if (process.platform === "win32") {
let filePersistence = await msalExt.FilePersistenceWithDataProtection.create(
cachePath,
msalExt.DataProtectionScope.LocalMachine
);

return {
cachePlugin: new msalExt.PersistenceCachePlugin(filePersistence)
}
}

// On Mac, uses keychain.
if (process.platform === "darwin") {
let keychainPersistence = await msalExt.KeychainPersistence.create(cachePath, "serviceName", "accountName");

return {
cachePlugin: new msalExt.PersistenceCachePlugin(keychainPersistence)
}
}

// On Linux, uses libsecret to store to secret service. Libsecret has to be installed.
if (process.platform === "linux") {
let libSecretPersistence = await msalExt.LibSecretPersistence.create(cachePath, "serviceName", "accountName");

return {
cachePlugin: new msalExt.PersistenceCachePlugin(libSecretPersistence)
}
}

// fall back to using plain text file. Not recommended for storing secrets.
let filePersistence = await msalExt.FilePersistence.create(cachePath);

return {
cachePlugin: new msalExt.PersistenceCachePlugin(filePersistence)
}
}

/**
* The record to use to find the cached tokens in the cache
*/
export interface AuthenticationRecord {
/**
* The associated authority, if used
*/
authority?: string;

/**
* The home account Id
*/
homeAccountId: string;

/**
* The login environment, eg "login.windows.net"
*/
environment: string;

/**
* The associated tenant ID
*/
tenantId: string;

/**
* The username of the logged in account
*/
username: string;
}

export class AuthenticationRequired extends CredentialUnavailable {}

export class MsalClient {
private persistenceEnabled: boolean;
private authenticationRecord: AuthenticationRecord | undefined;
private identityClient: IdentityClient;
private pca: PublicClientApplication | undefined;
private clientId: string;
private tenantId: string;
private authorityHost: string;
private cachePath?: string;

constructor(clientId: string, tenantId: string, authorityHost: string, persistenceEnabled: boolean, authenticationRecord?: AuthenticationRecord, cachePath?: string, options?: TokenCredentialOptions) {
this.identityClient = new IdentityClient(options);
this.clientId = clientId;
this.tenantId = tenantId;
this.authorityHost = authorityHost;
this.cachePath = cachePath;
this.persistenceEnabled = persistenceEnabled;
this.authenticationRecord = authenticationRecord;
}

async preparePublicClientApplication() {
// If we've already initialized the public client application, return
if (this.pca) {
return;
}

// If we need to load the plugin that handles persistence, go ahead and load it
let plugin = undefined;
if (this.persistenceEnabled && this.authenticationRecord) {
plugin = await createPersistence(this.cachePath);
}

// Construct the public client application, since it hasn't been initialized, yet
const knownAuthorities = this.tenantId === "adfs" ? [this.authorityHost] : [];
const publicClientConfig: Configuration = {
auth: {
clientId: this.clientId,
authority: this.authorityHost,
knownAuthorities: knownAuthorities
},
cache: plugin,
system: { networkClient: this.identityClient }
};
this.pca = new PublicClientApplication(publicClientConfig);
this.pca.getAuthCodeUrl
}

async acquireTokenFromCache(): Promise<AccessToken | null> {
if (!this.persistenceEnabled || !this.authenticationRecord) {
throw new AuthenticationRequired();
}

await this.preparePublicClientApplication();

const silentRequest = {
account: this.authenticationRecord!,
scopes: ["https://vault.azure.net/user_impersonation", "https://vault.azure.net/.default"]
};

try {
const response = await this.pca!.acquireTokenSilent(silentRequest);
logger.info("Successful silent token acquisition");
return {
expiresOnTimestamp: response.expiresOn.getTime(),
token: response.accessToken
};
} catch (e) {
throw new AuthenticationRequired("Could not authenticate silently using the cache");
}
}

async getAuthCodeUrl(request: { scopes: string[], redirectUri: string }): Promise<string> {
await this.preparePublicClientApplication();

return this.pca!.getAuthCodeUrl(request);
}

async acquireTokenByCode(request: AuthorizationCodeRequest): Promise<AuthenticationResult> {
return this.pca!.acquireTokenByCode(request);
}
}
33 changes: 0 additions & 33 deletions sdk/identity/identity/src/credentials/authentication.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { AccessToken, TokenCredential, GetTokenOptions } from "@azure/core-http";
import { AuthenticationRecord, AuthenticationRequired } from "./authentication";
import { AuthenticationRecord, AuthenticationRequired } from "../client/msalClient";
import { DeviceCodeCredentialOptions } from "./deviceCodeCredentialOptions";
import { createSpan } from "../util/tracing";
import { credentialLogger } from "../util/logging";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,12 @@
import { TokenCredential, GetTokenOptions, AccessToken } from "@azure/core-http";
import { InteractiveBrowserCredentialOptions } from "./interactiveBrowserCredentialOptions";
import { credentialLogger } from "../util/logging";
import { IdentityClient } from "../client/identityClient";
import { DefaultTenantId, DeveloperSignOnClientId } from "../constants";
import { Socket } from "net";
import { AuthenticationRecord, AuthenticationRequired } from "./authentication";
import { AuthenticationRequired, MsalClient } from "../client/msalClient";
import { AuthorizationCodeRequest } from "@azure/msal-node"

import express from "express";
import {
PublicClientApplication,
TokenCache,
AuthorizationCodeRequest,
Configuration
} from "@azure/msal-node";
import open from "open";
import http from "http";

Expand All @@ -29,24 +23,16 @@ const logger = credentialLogger("InteractiveBrowserCredential");
* window. This credential is not currently supported in Node.js.
*/
export class InteractiveBrowserCredential implements TokenCredential {
private identityClient: IdentityClient;
private pca: PublicClientApplication;
private msalCacheManager: TokenCache;
private tenantId: string;
private clientId: string;
private persistenceEnabled: boolean;
private redirectUri: string;
private authorityHost: string;
private authenticationRecord: AuthenticationRecord | undefined;
private port: number;
private msalClient: MsalClient;

constructor(options?: InteractiveBrowserCredentialOptions) {
this.identityClient = new IdentityClient(options);
this.tenantId = (options && options.tenantId) || DefaultTenantId;
this.clientId = (options && options.clientId) || DeveloperSignOnClientId;
let tenantId = (options && options.tenantId) || DefaultTenantId;
let clientId = (options && options.clientId) || DeveloperSignOnClientId;

this.persistenceEnabled = this.persistenceEnabled = options?.cacheOptions !== undefined;
this.authenticationRecord = options?.authenticationRecord;
let persistenceEnabled = options?.persistenceEnabled ? options?.persistenceEnabled : false;
let authenticationRecord = options?.authenticationRecord;

if (options && options.redirectUri) {
if (typeof options.redirectUri === "string") {
Expand All @@ -64,29 +50,18 @@ export class InteractiveBrowserCredential implements TokenCredential {
this.port = 80;
}

let authorityHost;
if (options && options.authorityHost) {
if (options.authorityHost.endsWith("/")) {
this.authorityHost = options.authorityHost + this.tenantId;
authorityHost = options.authorityHost + tenantId;
} else {
this.authorityHost = options.authorityHost + "/" + this.tenantId;
authorityHost = options.authorityHost + "/" + tenantId;
}
} else {
this.authorityHost = "https://login.microsoftonline.com/" + this.tenantId;
authorityHost = "https://login.microsoftonline.com/" + tenantId;
}

const knownAuthorities = this.tenantId === "adfs" ? [this.authorityHost] : [];

const publicClientConfig: Configuration = {
auth: {
clientId: this.clientId,
authority: this.authorityHost,
knownAuthorities: knownAuthorities
},
cache: options?.cacheOptions,
system: { networkClient: this.identityClient }
};
this.pca = new PublicClientApplication(publicClientConfig);
this.msalCacheManager = this.pca.getTokenCache();
this.msalClient = new MsalClient(clientId, tenantId, authorityHost, persistenceEnabled, authenticationRecord, ".", options);
}

/**
Expand All @@ -105,37 +80,13 @@ export class InteractiveBrowserCredential implements TokenCredential {
): Promise<AccessToken | null> {
const scopeArray = typeof scopes === "object" ? scopes : [scopes];

if (this.authenticationRecord && this.persistenceEnabled) {
return this.acquireTokenFromCache().catch((e) => {
if (e instanceof AuthenticationRequired) {
return this.acquireTokenFromBrowser(scopeArray);
} else {
throw e;
}
});
} else {
return this.acquireTokenFromBrowser(scopeArray);
}
}

private async acquireTokenFromCache(): Promise<AccessToken | null> {
await this.msalCacheManager.readFromPersistence();

const silentRequest = {
account: this.authenticationRecord!,
scopes: ["https://vault.azure.net/user_impersonation", "https://vault.azure.net/.default"]
};

try {
const response = await this.pca.acquireTokenSilent(silentRequest);
logger.info("Successful silent token acquisition");
return {
expiresOnTimestamp: response.expiresOn.getTime(),
token: response.accessToken
};
} catch (e) {
throw new AuthenticationRequired("Could not authenticate silently using the cache");
}
return this.msalClient.acquireTokenFromCache().catch((e) => {
if (e instanceof AuthenticationRequired) {
return this.acquireTokenFromBrowser(scopeArray);
} else {
throw e;
}
});
}

private async openAuthCodeUrl(scopeArray: string[]): Promise<void> {
Expand All @@ -144,12 +95,8 @@ export class InteractiveBrowserCredential implements TokenCredential {
redirectUri: this.redirectUri
};

const response = await this.pca.getAuthCodeUrl(authCodeUrlParameters);
const response = await this.msalClient.getAuthCodeUrl(authCodeUrlParameters);
await open(response);

if (this.persistenceEnabled) {
await this.msalCacheManager.readFromPersistence();
}
}

private async acquireTokenFromBrowser(scopeArray: string[]): Promise<AccessToken | null> {
Expand Down Expand Up @@ -179,13 +126,9 @@ export class InteractiveBrowserCredential implements TokenCredential {
};

try {
const authResponse = await this.pca.acquireTokenByCode(tokenRequest);
const authResponse = await this.msalClient.acquireTokenByCode(tokenRequest);
res.sendStatus(200);

if (this.persistenceEnabled) {
this.msalCacheManager.writeToPersistence();
}

resolve({
expiresOnTimestamp: authResponse.expiresOn.valueOf(),
token: authResponse.accessToken
Expand Down
Loading

0 comments on commit e9342e8

Please sign in to comment.