Add PBandGraphOauth scaffold and auth UI

This commit is contained in:
2025-12-19 05:42:29 +00:00
commit b51963f5d7
6264 changed files with 606430 additions and 0 deletions
+624
View File
@@ -0,0 +1,624 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AuthorizationCodeClient,
ClientConfiguration,
RefreshTokenClient,
AuthenticationResult,
Authority,
AuthorityFactory,
BaseAuthRequest,
SilentFlowClient,
Logger,
ServerTelemetryManager,
ServerTelemetryRequest,
CommonSilentFlowRequest,
CommonRefreshTokenRequest,
CommonAuthorizationCodeRequest,
CommonAuthorizationUrlRequest,
CommonUsernamePasswordRequest,
AuthenticationScheme,
ResponseMode,
AuthorityOptions,
OIDC_DEFAULT_SCOPES,
AzureRegionConfiguration,
AuthError,
AzureCloudOptions,
AuthorizationCodePayload,
Constants,
StringUtils,
createClientAuthError,
ClientAuthErrorCodes,
buildStaticAuthorityOptions,
ClientAssertion as ClientAssertionType,
getClientAssertion,
ClientAssertionCallback,
} from "@azure/msal-common/node";
import {
Configuration,
buildAppConfiguration,
NodeConfiguration,
} from "../config/Configuration.js";
import { CryptoProvider } from "../crypto/CryptoProvider.js";
import { NodeStorage } from "../cache/NodeStorage.js";
import { Constants as NodeConstants, ApiId } from "../utils/Constants.js";
import { TokenCache } from "../cache/TokenCache.js";
import { ClientAssertion } from "./ClientAssertion.js";
import { AuthorizationUrlRequest } from "../request/AuthorizationUrlRequest.js";
import { AuthorizationCodeRequest } from "../request/AuthorizationCodeRequest.js";
import { RefreshTokenRequest } from "../request/RefreshTokenRequest.js";
import { SilentFlowRequest } from "../request/SilentFlowRequest.js";
import { version, name } from "../packageMetadata.js";
import { UsernamePasswordRequest } from "../request/UsernamePasswordRequest.js";
import { NodeAuthError } from "../error/NodeAuthError.js";
import { UsernamePasswordClient } from "./UsernamePasswordClient.js";
/**
* Base abstract class for all ClientApplications - public and confidential
* @public
*/
export abstract class ClientApplication {
protected readonly cryptoProvider: CryptoProvider;
private tokenCache: TokenCache;
/**
* Platform storage object
*/
protected storage: NodeStorage;
/**
* Logger object to log the application flow
*/
protected logger: Logger;
/**
* Platform configuration initialized by the application
*/
protected config: NodeConfiguration;
/**
* Client assertion passed by the user for confidential client flows
*/
protected clientAssertion: ClientAssertion;
protected developerProvidedClientAssertion:
| string
| ClientAssertionCallback;
/**
* Client secret passed by the user for confidential client flows
*/
protected clientSecret: string;
/**
* Constructor for the ClientApplication
*/
protected constructor(configuration: Configuration) {
this.config = buildAppConfiguration(configuration);
this.cryptoProvider = new CryptoProvider();
this.logger = new Logger(
this.config.system.loggerOptions,
name,
version
);
this.storage = new NodeStorage(
this.logger,
this.config.auth.clientId,
this.cryptoProvider,
buildStaticAuthorityOptions(this.config.auth)
);
this.tokenCache = new TokenCache(
this.storage,
this.logger,
this.config.cache.cachePlugin
);
}
/**
* Creates the URL of the authorization request, letting the user input credentials and consent to the
* application. The URL targets the /authorize endpoint of the authority configured in the
* application object.
*
* Once the user inputs their credentials and consents, the authority will send a response to the redirect URI
* sent in the request and should contain an authorization code, which can then be used to acquire tokens via
* `acquireTokenByCode(AuthorizationCodeRequest)`.
*/
async getAuthCodeUrl(request: AuthorizationUrlRequest): Promise<string> {
this.logger.info("getAuthCodeUrl called", request.correlationId);
const validRequest: CommonAuthorizationUrlRequest = {
...request,
...(await this.initializeBaseRequest(request)),
responseMode: request.responseMode || ResponseMode.QUERY,
authenticationScheme: AuthenticationScheme.BEARER,
};
const authClientConfig = await this.buildOauthClientConfiguration(
validRequest.authority,
validRequest.correlationId,
validRequest.redirectUri,
undefined,
undefined,
request.azureCloudOptions
);
const authorizationCodeClient = new AuthorizationCodeClient(
authClientConfig
);
this.logger.verbose(
"Auth code client created",
validRequest.correlationId
);
return authorizationCodeClient.getAuthCodeUrl(validRequest);
}
/**
* Acquires a token by exchanging the Authorization Code received from the first step of OAuth2.0
* Authorization Code flow.
*
* `getAuthCodeUrl(AuthorizationCodeUrlRequest)` can be used to create the URL for the first step of OAuth2.0
* Authorization Code flow. Ensure that values for redirectUri and scopes in AuthorizationCodeUrlRequest and
* AuthorizationCodeRequest are the same.
*/
async acquireTokenByCode(
request: AuthorizationCodeRequest,
authCodePayLoad?: AuthorizationCodePayload
): Promise<AuthenticationResult> {
this.logger.info("acquireTokenByCode called");
if (request.state && authCodePayLoad) {
this.logger.info("acquireTokenByCode - validating state");
this.validateState(request.state, authCodePayLoad.state || "");
// eslint-disable-next-line no-param-reassign
authCodePayLoad = { ...authCodePayLoad, state: "" };
}
const validRequest: CommonAuthorizationCodeRequest = {
...request,
...(await this.initializeBaseRequest(request)),
authenticationScheme: AuthenticationScheme.BEARER,
};
const serverTelemetryManager = this.initializeServerTelemetryManager(
ApiId.acquireTokenByCode,
validRequest.correlationId
);
try {
const authClientConfig = await this.buildOauthClientConfiguration(
validRequest.authority,
validRequest.correlationId,
validRequest.redirectUri,
serverTelemetryManager,
undefined,
request.azureCloudOptions
);
const authorizationCodeClient = new AuthorizationCodeClient(
authClientConfig
);
this.logger.verbose(
"Auth code client created",
validRequest.correlationId
);
return await authorizationCodeClient.acquireToken(
validRequest,
authCodePayLoad
);
} catch (e) {
if (e instanceof AuthError) {
e.setCorrelationId(validRequest.correlationId);
}
serverTelemetryManager.cacheFailedRequest(e);
throw e;
}
}
/**
* Acquires a token by exchanging the refresh token provided for a new set of tokens.
*
* This API is provided only for scenarios where you would like to migrate from ADAL to MSAL. Otherwise, it is
* recommended that you use `acquireTokenSilent()` for silent scenarios. When using `acquireTokenSilent()`, MSAL will
* handle the caching and refreshing of tokens automatically.
*/
async acquireTokenByRefreshToken(
request: RefreshTokenRequest
): Promise<AuthenticationResult | null> {
this.logger.info(
"acquireTokenByRefreshToken called",
request.correlationId
);
const validRequest: CommonRefreshTokenRequest = {
...request,
...(await this.initializeBaseRequest(request)),
authenticationScheme: AuthenticationScheme.BEARER,
};
const serverTelemetryManager = this.initializeServerTelemetryManager(
ApiId.acquireTokenByRefreshToken,
validRequest.correlationId
);
try {
const refreshTokenClientConfig =
await this.buildOauthClientConfiguration(
validRequest.authority,
validRequest.correlationId,
validRequest.redirectUri || "",
serverTelemetryManager,
undefined,
request.azureCloudOptions
);
const refreshTokenClient = new RefreshTokenClient(
refreshTokenClientConfig
);
this.logger.verbose(
"Refresh token client created",
validRequest.correlationId
);
return await refreshTokenClient.acquireToken(validRequest);
} catch (e) {
if (e instanceof AuthError) {
e.setCorrelationId(validRequest.correlationId);
}
serverTelemetryManager.cacheFailedRequest(e);
throw e;
}
}
/**
* Acquires a token silently when a user specifies the account the token is requested for.
*
* This API expects the user to provide an account object and looks into the cache to retrieve the token if present.
* There is also an optional "forceRefresh" boolean the user can send to bypass the cache for access_token and id_token.
* In case the refresh_token is expired or not found, an error is thrown
* and the guidance is for the user to call any interactive token acquisition API (eg: `acquireTokenByCode()`).
*/
async acquireTokenSilent(
request: SilentFlowRequest
): Promise<AuthenticationResult> {
const validRequest: CommonSilentFlowRequest = {
...request,
...(await this.initializeBaseRequest(request)),
forceRefresh: request.forceRefresh || false,
};
const serverTelemetryManager = this.initializeServerTelemetryManager(
ApiId.acquireTokenSilent,
validRequest.correlationId,
validRequest.forceRefresh
);
try {
const silentFlowClientConfig =
await this.buildOauthClientConfiguration(
validRequest.authority,
validRequest.correlationId,
validRequest.redirectUri || "",
serverTelemetryManager,
undefined,
request.azureCloudOptions
);
const silentFlowClient = new SilentFlowClient(
silentFlowClientConfig
);
this.logger.verbose(
"Silent flow client created",
validRequest.correlationId
);
return await silentFlowClient.acquireToken(validRequest);
} catch (e) {
if (e instanceof AuthError) {
e.setCorrelationId(validRequest.correlationId);
}
serverTelemetryManager.cacheFailedRequest(e as AuthError);
throw e;
}
}
/**
* Acquires tokens with password grant by exchanging client applications username and password for credentials
*
* The latest OAuth 2.0 Security Best Current Practice disallows the password grant entirely.
* More details on this recommendation at https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-3.4
* Microsoft's documentation and recommendations are at:
* https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#usernamepassword
*
* @param request - UsenamePasswordRequest
*/
async acquireTokenByUsernamePassword(
request: UsernamePasswordRequest
): Promise<AuthenticationResult | null> {
this.logger.info(
"acquireTokenByUsernamePassword called",
request.correlationId
);
const validRequest: CommonUsernamePasswordRequest = {
...request,
...(await this.initializeBaseRequest(request)),
};
const serverTelemetryManager = this.initializeServerTelemetryManager(
ApiId.acquireTokenByUsernamePassword,
validRequest.correlationId
);
try {
const usernamePasswordClientConfig =
await this.buildOauthClientConfiguration(
validRequest.authority,
validRequest.correlationId,
"",
serverTelemetryManager,
undefined,
request.azureCloudOptions
);
const usernamePasswordClient = new UsernamePasswordClient(
usernamePasswordClientConfig
);
this.logger.verbose(
"Username password client created",
validRequest.correlationId
);
return await usernamePasswordClient.acquireToken(validRequest);
} catch (e) {
if (e instanceof AuthError) {
e.setCorrelationId(validRequest.correlationId);
}
serverTelemetryManager.cacheFailedRequest(e);
throw e;
}
}
/**
* Gets the token cache for the application.
*/
getTokenCache(): TokenCache {
this.logger.info("getTokenCache called");
return this.tokenCache;
}
/**
* Validates OIDC state by comparing the user cached state with the state received from the server.
*
* This API is provided for scenarios where you would use OAuth2.0 state parameter to mitigate against
* CSRF attacks.
* For more information about state, visit https://datatracker.ietf.org/doc/html/rfc6819#section-3.6.
* @param state - Unique GUID generated by the user that is cached by the user and sent to the server during the first leg of the flow
* @param cachedState - This string is sent back by the server with the authorization code
*/
protected validateState(state: string, cachedState: string): void {
if (!state) {
throw NodeAuthError.createStateNotFoundError();
}
if (state !== cachedState) {
throw createClientAuthError(ClientAuthErrorCodes.stateMismatch);
}
}
/**
* Returns the logger instance
*/
getLogger(): Logger {
return this.logger;
}
/**
* Replaces the default logger set in configurations with new Logger with new configurations
* @param logger - Logger instance
*/
setLogger(logger: Logger): void {
this.logger = logger;
}
/**
* Builds the common configuration to be passed to the common component based on the platform configurarion
* @param authority - user passed authority in configuration
* @param serverTelemetryManager - initializes servertelemetry if passed
*/
protected async buildOauthClientConfiguration(
authority: string,
requestCorrelationId: string,
redirectUri: string,
serverTelemetryManager?: ServerTelemetryManager,
azureRegionConfiguration?: AzureRegionConfiguration,
azureCloudOptions?: AzureCloudOptions
): Promise<ClientConfiguration> {
this.logger.verbose(
"buildOauthClientConfiguration called",
requestCorrelationId
);
// precedence - azureCloudInstance + tenant >> authority and request >> config
const userAzureCloudOptions = azureCloudOptions
? azureCloudOptions
: this.config.auth.azureCloudOptions;
// using null assertion operator as we ensure that all config values have default values in buildConfiguration()
const discoveredAuthority = await this.createAuthority(
authority,
requestCorrelationId,
azureRegionConfiguration,
userAzureCloudOptions
);
this.logger.info(
`Building oauth client configuration with the following authority: ${discoveredAuthority.tokenEndpoint}.`,
requestCorrelationId
);
serverTelemetryManager?.updateRegionDiscoveryMetadata(
discoveredAuthority.regionDiscoveryMetadata
);
const clientConfiguration: ClientConfiguration = {
authOptions: {
clientId: this.config.auth.clientId,
authority: discoveredAuthority,
clientCapabilities: this.config.auth.clientCapabilities,
redirectUri,
},
loggerOptions: {
logLevel: this.config.system.loggerOptions.logLevel,
loggerCallback: this.config.system.loggerOptions.loggerCallback,
piiLoggingEnabled:
this.config.system.loggerOptions.piiLoggingEnabled,
correlationId: requestCorrelationId,
},
cacheOptions: {
claimsBasedCachingEnabled:
this.config.cache.claimsBasedCachingEnabled,
},
cryptoInterface: this.cryptoProvider,
networkInterface: this.config.system.networkClient,
storageInterface: this.storage,
serverTelemetryManager: serverTelemetryManager,
clientCredentials: {
clientSecret: this.clientSecret,
clientAssertion: await this.getClientAssertion(
discoveredAuthority
),
},
libraryInfo: {
sku: NodeConstants.MSAL_SKU,
version: version,
cpu: process.arch || Constants.EMPTY_STRING,
os: process.platform || Constants.EMPTY_STRING,
},
telemetry: this.config.telemetry,
persistencePlugin: this.config.cache.cachePlugin,
serializableCache: this.tokenCache,
};
return clientConfiguration;
}
private async getClientAssertion(
authority: Authority
): Promise<ClientAssertionType> {
if (this.developerProvidedClientAssertion) {
this.clientAssertion = ClientAssertion.fromAssertion(
await getClientAssertion(
this.developerProvidedClientAssertion,
this.config.auth.clientId,
authority.tokenEndpoint
)
);
}
return (
this.clientAssertion && {
assertion: this.clientAssertion.getJwt(
this.cryptoProvider,
this.config.auth.clientId,
authority.tokenEndpoint
),
assertionType: NodeConstants.JWT_BEARER_ASSERTION_TYPE,
}
);
}
/**
* Generates a request with the default scopes & generates a correlationId.
* @param authRequest - BaseAuthRequest for initialization
*/
protected async initializeBaseRequest(
authRequest: Partial<BaseAuthRequest>
): Promise<BaseAuthRequest> {
this.logger.verbose(
"initializeRequestScopes called",
authRequest.correlationId
);
// Default authenticationScheme to Bearer, log that POP isn't supported yet
if (
authRequest.authenticationScheme &&
authRequest.authenticationScheme === AuthenticationScheme.POP
) {
this.logger.verbose(
"Authentication Scheme 'pop' is not supported yet, setting Authentication Scheme to 'Bearer' for request",
authRequest.correlationId
);
}
authRequest.authenticationScheme = AuthenticationScheme.BEARER;
// Set requested claims hash if claims-based caching is enabled and claims were requested
if (
this.config.cache.claimsBasedCachingEnabled &&
authRequest.claims &&
// Checks for empty stringified object "{}" which doesn't qualify as requested claims
!StringUtils.isEmptyObj(authRequest.claims)
) {
authRequest.requestedClaimsHash =
await this.cryptoProvider.hashString(authRequest.claims);
}
return {
...authRequest,
scopes: [
...((authRequest && authRequest.scopes) || []),
...OIDC_DEFAULT_SCOPES,
],
correlationId:
(authRequest && authRequest.correlationId) ||
this.cryptoProvider.createNewGuid(),
authority: authRequest.authority || this.config.auth.authority,
};
}
/**
* Initializes the server telemetry payload
* @param apiId - Id for a specific request
* @param correlationId - GUID
* @param forceRefresh - boolean to indicate network call
*/
protected initializeServerTelemetryManager(
apiId: number,
correlationId: string,
forceRefresh?: boolean
): ServerTelemetryManager {
const telemetryPayload: ServerTelemetryRequest = {
clientId: this.config.auth.clientId,
correlationId: correlationId,
apiId: apiId,
forceRefresh: forceRefresh || false,
};
return new ServerTelemetryManager(telemetryPayload, this.storage);
}
/**
* Create authority instance. If authority not passed in request, default to authority set on the application
* object. If no authority set in application object, then default to common authority.
* @param authorityString - authority from user configuration
*/
private async createAuthority(
authorityString: string,
requestCorrelationId: string,
azureRegionConfiguration?: AzureRegionConfiguration,
azureCloudOptions?: AzureCloudOptions
): Promise<Authority> {
this.logger.verbose("createAuthority called", requestCorrelationId);
// build authority string based on auth params - azureCloudInstance is prioritized if provided
const authorityUrl = Authority.generateAuthority(
authorityString,
azureCloudOptions
);
const authorityOptions: AuthorityOptions = {
protocolMode: this.config.auth.protocolMode,
knownAuthorities: this.config.auth.knownAuthorities,
cloudDiscoveryMetadata: this.config.auth.cloudDiscoveryMetadata,
authorityMetadata: this.config.auth.authorityMetadata,
azureRegionConfiguration,
skipAuthorityMetadataCache:
this.config.auth.skipAuthorityMetadataCache,
};
return AuthorityFactory.createDiscoveredInstance(
authorityUrl,
this.config.system.networkClient,
this.storage,
authorityOptions,
this.logger,
requestCorrelationId
);
}
/**
* Clear the cache
*/
clearCache(): void {
this.storage.clear();
}
}
+202
View File
@@ -0,0 +1,202 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import jwt from "jsonwebtoken";
import {
TimeUtils,
Constants,
createClientAuthError,
ClientAuthErrorCodes,
} from "@azure/msal-common/node";
import { CryptoProvider } from "../crypto/CryptoProvider.js";
import { EncodingUtils } from "../utils/EncodingUtils.js";
import { JwtConstants } from "../utils/Constants.js";
/**
* Client assertion of type jwt-bearer used in confidential client flows
* @public
*/
export class ClientAssertion {
private jwt: string;
private privateKey: string;
private thumbprint: string;
private useSha256: boolean;
private expirationTime: number;
private issuer: string;
private jwtAudience: string;
private publicCertificate: Array<string>;
/**
* Initialize the ClientAssertion class from the clientAssertion passed by the user
* @param assertion - refer https://tools.ietf.org/html/rfc7521
*/
public static fromAssertion(assertion: string): ClientAssertion {
const clientAssertion = new ClientAssertion();
clientAssertion.jwt = assertion;
return clientAssertion;
}
/**
* @deprecated Use fromCertificateWithSha256Thumbprint instead, with a SHA-256 thumprint
* Initialize the ClientAssertion class from the certificate passed by the user
* @param thumbprint - identifier of a certificate
* @param privateKey - secret key
* @param publicCertificate - electronic document provided to prove the ownership of the public key
*/
public static fromCertificate(
thumbprint: string,
privateKey: string,
publicCertificate?: string
): ClientAssertion {
const clientAssertion = new ClientAssertion();
clientAssertion.privateKey = privateKey;
clientAssertion.thumbprint = thumbprint;
clientAssertion.useSha256 = false;
if (publicCertificate) {
clientAssertion.publicCertificate =
this.parseCertificate(publicCertificate);
}
return clientAssertion;
}
/**
* Initialize the ClientAssertion class from the certificate passed by the user
* @param thumbprint - identifier of a certificate
* @param privateKey - secret key
* @param publicCertificate - electronic document provided to prove the ownership of the public key
*/
public static fromCertificateWithSha256Thumbprint(
thumbprint: string,
privateKey: string,
publicCertificate?: string
): ClientAssertion {
const clientAssertion = new ClientAssertion();
clientAssertion.privateKey = privateKey;
clientAssertion.thumbprint = thumbprint;
clientAssertion.useSha256 = true;
if (publicCertificate) {
clientAssertion.publicCertificate =
this.parseCertificate(publicCertificate);
}
return clientAssertion;
}
/**
* Update JWT for certificate based clientAssertion, if passed by the user, uses it as is
* @param cryptoProvider - library's crypto helper
* @param issuer - iss claim
* @param jwtAudience - aud claim
*/
public getJwt(
cryptoProvider: CryptoProvider,
issuer: string,
jwtAudience: string
): string {
// if assertion was created from certificate, check if jwt is expired and create new one.
if (this.privateKey && this.thumbprint) {
if (
this.jwt &&
!this.isExpired() &&
issuer === this.issuer &&
jwtAudience === this.jwtAudience
) {
return this.jwt;
}
return this.createJwt(cryptoProvider, issuer, jwtAudience);
}
/*
* if assertion was created by caller, then we just append it. It is up to the caller to
* ensure that it contains necessary claims and that it is not expired.
*/
if (this.jwt) {
return this.jwt;
}
throw createClientAuthError(ClientAuthErrorCodes.invalidAssertion);
}
/**
* JWT format and required claims specified: https://tools.ietf.org/html/rfc7523#section-3
*/
private createJwt(
cryptoProvider: CryptoProvider,
issuer: string,
jwtAudience: string
): string {
this.issuer = issuer;
this.jwtAudience = jwtAudience;
const issuedAt = TimeUtils.nowSeconds();
this.expirationTime = issuedAt + 600;
const algorithm = this.useSha256
? JwtConstants.PSS_256
: JwtConstants.RSA_256;
const header: jwt.JwtHeader = {
alg: algorithm,
};
const thumbprintHeader = this.useSha256
? JwtConstants.X5T_256
: JwtConstants.X5T;
Object.assign(header, {
[thumbprintHeader]: EncodingUtils.base64EncodeUrl(
this.thumbprint,
"hex"
),
} as Partial<jwt.JwtHeader>);
if (this.publicCertificate) {
Object.assign(header, {
[JwtConstants.X5C]: this.publicCertificate,
} as Partial<jwt.JwtHeader>);
}
const payload = {
[JwtConstants.AUDIENCE]: this.jwtAudience,
[JwtConstants.EXPIRATION_TIME]: this.expirationTime,
[JwtConstants.ISSUER]: this.issuer,
[JwtConstants.SUBJECT]: this.issuer,
[JwtConstants.NOT_BEFORE]: issuedAt,
[JwtConstants.JWT_ID]: cryptoProvider.createNewGuid(),
};
this.jwt = jwt.sign(payload, this.privateKey, { header });
return this.jwt;
}
/**
* Utility API to check expiration
*/
private isExpired(): boolean {
return this.expirationTime < TimeUtils.nowSeconds();
}
/**
* Extracts the raw certs from a given certificate string and returns them in an array.
* @param publicCertificate - electronic document provided to prove the ownership of the public key
*/
public static parseCertificate(publicCertificate: string): Array<string> {
/**
* This is regex to identify the certs in a given certificate string.
* We want to look for the contents between the BEGIN and END certificate strings, without the associated newlines.
* The information in parens "(.+?)" is the capture group to represent the cert we want isolated.
* "." means any string character, "+" means match 1 or more times, and "?" means the shortest match.
* The "g" at the end of the regex means search the string globally, and the "s" enables the "." to match newlines.
*/
const regexToFindCerts =
/-----BEGIN CERTIFICATE-----\r*\n(.+?)\r*\n-----END CERTIFICATE-----/gs;
const certs: string[] = [];
let matches;
while ((matches = regexToFindCerts.exec(publicCertificate)) !== null) {
// matches[1] represents the first parens capture group in the regex.
certs.push(matches[1].replace(/\r*\n/g, Constants.EMPTY_STRING));
}
return certs;
}
}
+404
View File
@@ -0,0 +1,404 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AccessTokenEntity,
AuthenticationResult,
AuthenticationScheme,
Authority,
BaseClient,
CacheManager,
CacheOutcome,
ClientAuthErrorCodes,
ClientConfiguration,
CommonClientCredentialRequest,
Constants,
CredentialFilter,
CredentialType,
DEFAULT_TOKEN_RENEWAL_OFFSET_SEC,
GrantType,
IAppTokenProvider,
ICrypto,
RequestParameterBuilder,
RequestThumbprint,
ResponseHandler,
ScopeSet,
ServerAuthorizationTokenResponse,
ServerTelemetryManager,
StringUtils,
TimeUtils,
TokenCacheContext,
UrlString,
createClientAuthError,
ClientAssertion,
getClientAssertion,
} from "@azure/msal-common/node";
import {
ManagedIdentityConfiguration,
ManagedIdentityNodeConfiguration,
} from "../config/Configuration.js";
/**
* OAuth2.0 client credential grant
* @public
*/
export class ClientCredentialClient extends BaseClient {
private readonly appTokenProvider?: IAppTokenProvider;
constructor(
configuration: ClientConfiguration,
appTokenProvider?: IAppTokenProvider
) {
super(configuration);
this.appTokenProvider = appTokenProvider;
}
/**
* Public API to acquire a token with ClientCredential Flow for Confidential clients
* @param request - CommonClientCredentialRequest provided by the developer
*/
public async acquireToken(
request: CommonClientCredentialRequest
): Promise<AuthenticationResult | null> {
if (request.skipCache || request.claims) {
return this.executeTokenRequest(request, this.authority);
}
const [cachedAuthenticationResult, lastCacheOutcome] =
await this.getCachedAuthenticationResult(
request,
this.config,
this.cryptoUtils,
this.authority,
this.cacheManager,
this.serverTelemetryManager
);
if (cachedAuthenticationResult) {
// if the token is not expired but must be refreshed; get a new one in the background
if (lastCacheOutcome === CacheOutcome.PROACTIVELY_REFRESHED) {
this.logger.info(
"ClientCredentialClient:getCachedAuthenticationResult - Cached access token's refreshOn property has been exceeded'. It's not expired, but must be refreshed."
);
// refresh the access token in the background
const refreshAccessToken = true;
await this.executeTokenRequest(
request,
this.authority,
refreshAccessToken
);
}
// return the cached token
return cachedAuthenticationResult;
} else {
return this.executeTokenRequest(request, this.authority);
}
}
/**
* looks up cache if the tokens are cached already
*/
public async getCachedAuthenticationResult(
request: CommonClientCredentialRequest,
config: ClientConfiguration | ManagedIdentityConfiguration,
cryptoUtils: ICrypto,
authority: Authority,
cacheManager: CacheManager,
serverTelemetryManager?: ServerTelemetryManager | null
): Promise<[AuthenticationResult | null, CacheOutcome]> {
const clientConfiguration = config as ClientConfiguration;
const managedIdentityConfiguration =
config as ManagedIdentityNodeConfiguration;
let lastCacheOutcome: CacheOutcome = CacheOutcome.NOT_APPLICABLE;
// read the user-supplied cache into memory, if applicable
let cacheContext;
if (
clientConfiguration.serializableCache &&
clientConfiguration.persistencePlugin
) {
cacheContext = new TokenCacheContext(
clientConfiguration.serializableCache,
false
);
await clientConfiguration.persistencePlugin.beforeCacheAccess(
cacheContext
);
}
const cachedAccessToken = this.readAccessTokenFromCache(
authority,
managedIdentityConfiguration.managedIdentityId?.id ||
clientConfiguration.authOptions.clientId,
new ScopeSet(request.scopes || []),
cacheManager,
request.correlationId
);
if (
clientConfiguration.serializableCache &&
clientConfiguration.persistencePlugin &&
cacheContext
) {
await clientConfiguration.persistencePlugin.afterCacheAccess(
cacheContext
);
}
// must refresh due to non-existent access_token
if (!cachedAccessToken) {
serverTelemetryManager?.setCacheOutcome(
CacheOutcome.NO_CACHED_ACCESS_TOKEN
);
return [null, CacheOutcome.NO_CACHED_ACCESS_TOKEN];
}
// must refresh due to the expires_in value
if (
TimeUtils.isTokenExpired(
cachedAccessToken.expiresOn,
clientConfiguration.systemOptions?.tokenRenewalOffsetSeconds ||
DEFAULT_TOKEN_RENEWAL_OFFSET_SEC
)
) {
serverTelemetryManager?.setCacheOutcome(
CacheOutcome.CACHED_ACCESS_TOKEN_EXPIRED
);
return [null, CacheOutcome.CACHED_ACCESS_TOKEN_EXPIRED];
}
// must refresh (in the background) due to the refresh_in value
if (
cachedAccessToken.refreshOn &&
TimeUtils.isTokenExpired(cachedAccessToken.refreshOn.toString(), 0)
) {
lastCacheOutcome = CacheOutcome.PROACTIVELY_REFRESHED;
serverTelemetryManager?.setCacheOutcome(
CacheOutcome.PROACTIVELY_REFRESHED
);
}
return [
await ResponseHandler.generateAuthenticationResult(
cryptoUtils,
authority,
{
account: null,
idToken: null,
accessToken: cachedAccessToken,
refreshToken: null,
appMetadata: null,
},
true,
request
),
lastCacheOutcome,
];
}
/**
* Reads access token from the cache
*/
private readAccessTokenFromCache(
authority: Authority,
id: string,
scopeSet: ScopeSet,
cacheManager: CacheManager,
correlationId: string
): AccessTokenEntity | null {
const accessTokenFilter: CredentialFilter = {
homeAccountId: Constants.EMPTY_STRING,
environment:
authority.canonicalAuthorityUrlComponents.HostNameAndPort,
credentialType: CredentialType.ACCESS_TOKEN,
clientId: id,
realm: authority.tenant,
target: ScopeSet.createSearchScopes(scopeSet.asArray()),
};
const accessTokens = cacheManager.getAccessTokensByFilter(
accessTokenFilter,
correlationId
);
if (accessTokens.length < 1) {
return null;
} else if (accessTokens.length > 1) {
throw createClientAuthError(
ClientAuthErrorCodes.multipleMatchingTokens
);
}
return accessTokens[0] as AccessTokenEntity;
}
/**
* Makes a network call to request the token from the service
* @param request - CommonClientCredentialRequest provided by the developer
* @param authority - authority object
*/
private async executeTokenRequest(
request: CommonClientCredentialRequest,
authority: Authority,
refreshAccessToken?: boolean
): Promise<AuthenticationResult | null> {
let serverTokenResponse: ServerAuthorizationTokenResponse;
let reqTimestamp: number;
if (this.appTokenProvider) {
this.logger.info("Using appTokenProvider extensibility.");
const appTokenPropviderParameters = {
correlationId: request.correlationId,
tenantId: this.config.authOptions.authority.tenant,
scopes: request.scopes,
claims: request.claims,
};
reqTimestamp = TimeUtils.nowSeconds();
const appTokenProviderResult = await this.appTokenProvider(
appTokenPropviderParameters
);
serverTokenResponse = {
access_token: appTokenProviderResult.accessToken,
expires_in: appTokenProviderResult.expiresInSeconds,
refresh_in: appTokenProviderResult.refreshInSeconds,
token_type: AuthenticationScheme.BEARER,
};
} else {
const queryParametersString =
this.createTokenQueryParameters(request);
const endpoint = UrlString.appendQueryString(
authority.tokenEndpoint,
queryParametersString
);
const requestBody = await this.createTokenRequestBody(request);
const headers: Record<string, string> =
this.createTokenRequestHeaders();
const thumbprint: RequestThumbprint = {
clientId: this.config.authOptions.clientId,
authority: request.authority,
scopes: request.scopes,
claims: request.claims,
authenticationScheme: request.authenticationScheme,
resourceRequestMethod: request.resourceRequestMethod,
resourceRequestUri: request.resourceRequestUri,
shrClaims: request.shrClaims,
sshKid: request.sshKid,
};
this.logger.info(
"Sending token request to endpoint: " + authority.tokenEndpoint
);
reqTimestamp = TimeUtils.nowSeconds();
const response = await this.executePostToTokenEndpoint(
endpoint,
requestBody,
headers,
thumbprint,
request.correlationId
);
serverTokenResponse = response.body;
serverTokenResponse.status = response.status;
}
const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
this.cacheManager,
this.cryptoUtils,
this.logger,
this.config.serializableCache,
this.config.persistencePlugin
);
responseHandler.validateTokenResponse(
serverTokenResponse,
refreshAccessToken
);
const tokenResponse = await responseHandler.handleServerTokenResponse(
serverTokenResponse,
this.authority,
reqTimestamp,
request
);
return tokenResponse;
}
/**
* generate the request to the server in the acceptable format
* @param request - CommonClientCredentialRequest provided by the developer
*/
private async createTokenRequestBody(
request: CommonClientCredentialRequest
): Promise<string> {
const parameterBuilder = new RequestParameterBuilder();
parameterBuilder.addClientId(this.config.authOptions.clientId);
parameterBuilder.addScopes(request.scopes, false);
parameterBuilder.addGrantType(GrantType.CLIENT_CREDENTIALS_GRANT);
parameterBuilder.addLibraryInfo(this.config.libraryInfo);
parameterBuilder.addApplicationTelemetry(
this.config.telemetry.application
);
parameterBuilder.addThrottling();
if (this.serverTelemetryManager) {
parameterBuilder.addServerTelemetry(this.serverTelemetryManager);
}
const correlationId =
request.correlationId ||
this.config.cryptoInterface.createNewGuid();
parameterBuilder.addCorrelationId(correlationId);
if (this.config.clientCredentials.clientSecret) {
parameterBuilder.addClientSecret(
this.config.clientCredentials.clientSecret
);
}
// Use clientAssertion from request, fallback to client assertion in base configuration
const clientAssertion: ClientAssertion | undefined =
request.clientAssertion ||
this.config.clientCredentials.clientAssertion;
if (clientAssertion) {
parameterBuilder.addClientAssertion(
await getClientAssertion(
clientAssertion.assertion,
this.config.authOptions.clientId,
request.resourceRequestUri
)
);
parameterBuilder.addClientAssertionType(
clientAssertion.assertionType
);
}
if (
!StringUtils.isEmptyObj(request.claims) ||
(this.config.authOptions.clientCapabilities &&
this.config.authOptions.clientCapabilities.length > 0)
) {
parameterBuilder.addClaims(
request.claims,
this.config.authOptions.clientCapabilities
);
}
return parameterBuilder.createQueryString();
}
}
@@ -0,0 +1,298 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
// AADAuthorityConstants
import { ClientApplication } from "./ClientApplication.js";
import { Configuration } from "../config/Configuration.js";
import { ClientAssertion } from "./ClientAssertion.js";
import {
Constants as NodeConstants,
ApiId,
REGION_ENVIRONMENT_VARIABLE,
MSAL_FORCE_REGION,
} from "../utils/Constants.js";
import {
CommonClientCredentialRequest,
CommonOnBehalfOfRequest,
AuthenticationResult,
AzureRegionConfiguration,
AuthError,
IAppTokenProvider,
OIDC_DEFAULT_SCOPES,
UrlString,
AADAuthorityConstants,
createClientAuthError,
ClientAuthErrorCodes,
ClientAssertion as ClientAssertionType,
getClientAssertion,
AzureRegion,
} from "@azure/msal-common/node";
import { IConfidentialClientApplication } from "./IConfidentialClientApplication.js";
import { OnBehalfOfRequest } from "../request/OnBehalfOfRequest.js";
import { ClientCredentialRequest } from "../request/ClientCredentialRequest.js";
import { ClientCredentialClient } from "./ClientCredentialClient.js";
import { OnBehalfOfClient } from "./OnBehalfOfClient.js";
/**
* This class is to be used to acquire tokens for confidential client applications (webApp, webAPI). Confidential client applications
* will configure application secrets, client certificates/assertions as applicable
* @public
*/
export class ConfidentialClientApplication
extends ClientApplication
implements IConfidentialClientApplication
{
private appTokenProvider?: IAppTokenProvider;
/**
* Constructor for the ConfidentialClientApplication
*
* Required attributes in the Configuration object are:
* - clientID: the application ID of your application. You can obtain one by registering your application with our application registration portal
* - authority: the authority URL for your application.
* - client credential: Must set either client secret, certificate, or assertion for confidential clients. You can obtain a client secret from the application registration portal.
*
* In Azure AD, authority is a URL indicating of the form https://login.microsoftonline.com/\{Enter_the_Tenant_Info_Here\}.
* If your application supports Accounts in one organizational directory, replace "Enter_the_Tenant_Info_Here" value with the Tenant Id or Tenant name (for example, contoso.microsoft.com).
* If your application supports Accounts in any organizational directory, replace "Enter_the_Tenant_Info_Here" value with organizations.
* If your application supports Accounts in any organizational directory and personal Microsoft accounts, replace "Enter_the_Tenant_Info_Here" value with common.
* To restrict support to Personal Microsoft accounts only, replace "Enter_the_Tenant_Info_Here" value with consumers.
*
* In Azure B2C, authority is of the form https://\{instance\}/tfp/\{tenant\}/\{policyName\}/
* Full B2C functionality will be available in this library in future versions.
*
* @param Configuration - configuration object for the MSAL ConfidentialClientApplication instance
*/
constructor(configuration: Configuration) {
super(configuration);
this.setClientCredential();
this.appTokenProvider = undefined;
}
/**
* This extensibility point only works for the client_credential flow, i.e. acquireTokenByClientCredential and
* is meant for Azure SDK to enhance Managed Identity support.
*
* @param IAppTokenProvider - Extensibility interface, which allows the app developer to return a token from a custom source.
*/
SetAppTokenProvider(provider: IAppTokenProvider): void {
this.appTokenProvider = provider;
}
/**
* Acquires tokens from the authority for the application (not for an end user).
*/
public async acquireTokenByClientCredential(
request: ClientCredentialRequest
): Promise<AuthenticationResult | null> {
this.logger.info(
"acquireTokenByClientCredential called",
request.correlationId
);
// If there is a client assertion present in the request, it overrides the one present in the client configuration
let clientAssertion: ClientAssertionType | undefined;
if (request.clientAssertion) {
clientAssertion = {
assertion: await getClientAssertion(
request.clientAssertion,
this.config.auth.clientId
// tokenEndpoint will be undefined. resourceRequestUri is omitted in ClientCredentialRequest
),
assertionType: NodeConstants.JWT_BEARER_ASSERTION_TYPE,
};
}
const baseRequest = await this.initializeBaseRequest(request);
// valid base request should not contain oidc scopes in this grant type
const validBaseRequest = {
...baseRequest,
scopes: baseRequest.scopes.filter(
(scope: string) => !OIDC_DEFAULT_SCOPES.includes(scope)
),
};
const validRequest: CommonClientCredentialRequest = {
...request,
...validBaseRequest,
clientAssertion,
};
/*
* valid request should not have "common" or "organizations" in lieu of the tenant_id in the authority in the auth configuration
* example authority: "https://login.microsoftonline.com/TenantId",
*/
const authority = new UrlString(validRequest.authority);
const tenantId = authority.getUrlComponents().PathSegments[0];
if (
Object.values(AADAuthorityConstants).includes(
tenantId as AADAuthorityConstants
)
) {
throw createClientAuthError(
ClientAuthErrorCodes.missingTenantIdError
);
}
/*
* if this env variable is set, and the developer provided region isn't defined and isn't "DisableMsalForceRegion",
* MSAL shall opt-in to ESTS-R with the value of this variable
*/
const ENV_MSAL_FORCE_REGION: AzureRegion | undefined =
process.env[MSAL_FORCE_REGION];
let region: AzureRegion | undefined;
if (validRequest.azureRegion !== "DisableMsalForceRegion") {
if (!validRequest.azureRegion && ENV_MSAL_FORCE_REGION) {
region = ENV_MSAL_FORCE_REGION;
} else {
region = validRequest.azureRegion;
}
}
const azureRegionConfiguration: AzureRegionConfiguration = {
azureRegion: region,
environmentRegion: process.env[REGION_ENVIRONMENT_VARIABLE],
};
const serverTelemetryManager = this.initializeServerTelemetryManager(
ApiId.acquireTokenByClientCredential,
validRequest.correlationId,
validRequest.skipCache
);
try {
const clientCredentialConfig =
await this.buildOauthClientConfiguration(
validRequest.authority,
validRequest.correlationId,
"",
serverTelemetryManager,
azureRegionConfiguration,
request.azureCloudOptions
);
const clientCredentialClient = new ClientCredentialClient(
clientCredentialConfig,
this.appTokenProvider
);
this.logger.verbose(
"Client credential client created",
validRequest.correlationId
);
return await clientCredentialClient.acquireToken(validRequest);
} catch (e) {
if (e instanceof AuthError) {
e.setCorrelationId(validRequest.correlationId);
}
serverTelemetryManager.cacheFailedRequest(e);
throw e;
}
}
/**
* Acquires tokens from the authority for the application.
*
* Used in scenarios where the current app is a middle-tier service which was called with a token
* representing an end user. The current app can use the token (oboAssertion) to request another
* token to access downstream web API, on behalf of that user.
*
* The current middle-tier app has no user interaction to obtain consent.
* See how to gain consent upfront for your middle-tier app from this article.
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#gaining-consent-for-the-middle-tier-application
*/
public async acquireTokenOnBehalfOf(
request: OnBehalfOfRequest
): Promise<AuthenticationResult | null> {
this.logger.info(
"acquireTokenOnBehalfOf called",
request.correlationId
);
const validRequest: CommonOnBehalfOfRequest = {
...request,
...(await this.initializeBaseRequest(request)),
};
try {
const onBehalfOfConfig = await this.buildOauthClientConfiguration(
validRequest.authority,
validRequest.correlationId,
"",
undefined,
undefined,
request.azureCloudOptions
);
const oboClient = new OnBehalfOfClient(onBehalfOfConfig);
this.logger.verbose(
"On behalf of client created",
validRequest.correlationId
);
return await oboClient.acquireToken(validRequest);
} catch (e) {
if (e instanceof AuthError) {
e.setCorrelationId(validRequest.correlationId);
}
throw e;
}
}
private setClientCredential(): void {
const clientSecretNotEmpty = !!this.config.auth.clientSecret;
const clientAssertionNotEmpty = !!this.config.auth.clientAssertion;
const certificateNotEmpty =
(!!this.config.auth.clientCertificate?.thumbprint ||
!!this.config.auth.clientCertificate?.thumbprintSha256) &&
!!this.config.auth.clientCertificate?.privateKey;
/*
* If app developer configures this callback, they don't need a credential
* i.e. AzureSDK can get token from Managed Identity without a cert / secret
*/
if (this.appTokenProvider) {
return;
}
// Check that at most one credential is set on the application
if (
(clientSecretNotEmpty && clientAssertionNotEmpty) ||
(clientAssertionNotEmpty && certificateNotEmpty) ||
(clientSecretNotEmpty && certificateNotEmpty)
) {
throw createClientAuthError(
ClientAuthErrorCodes.invalidClientCredential
);
}
if (this.config.auth.clientSecret) {
this.clientSecret = this.config.auth.clientSecret;
return;
}
if (this.config.auth.clientAssertion) {
this.developerProvidedClientAssertion =
this.config.auth.clientAssertion;
return;
}
if (!certificateNotEmpty) {
throw createClientAuthError(
ClientAuthErrorCodes.invalidClientCredential
);
} else {
this.clientAssertion = !!this.config.auth.clientCertificate
.thumbprintSha256
? ClientAssertion.fromCertificateWithSha256Thumbprint(
this.config.auth.clientCertificate.thumbprintSha256,
this.config.auth.clientCertificate.privateKey,
this.config.auth.clientCertificate.x5c
)
: ClientAssertion.fromCertificate(
// guaranteed to be a string, due to prior error checking in this function
this.config.auth.clientCertificate.thumbprint as string,
this.config.auth.clientCertificate.privateKey,
this.config.auth.clientCertificate.x5c
);
}
}
}
+378
View File
@@ -0,0 +1,378 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AuthErrorCodes,
AuthenticationResult,
BaseClient,
ClientAuthErrorCodes,
ClientConfiguration,
CommonDeviceCodeRequest,
Constants,
DeviceCodeResponse,
GrantType,
RequestParameterBuilder,
RequestThumbprint,
ResponseHandler,
ServerAuthorizationTokenResponse,
ServerDeviceCodeResponse,
StringUtils,
TimeUtils,
UrlString,
createAuthError,
createClientAuthError,
} from "@azure/msal-common/node";
/**
* OAuth2.0 Device code client
* @public
*/
export class DeviceCodeClient extends BaseClient {
constructor(configuration: ClientConfiguration) {
super(configuration);
}
/**
* Gets device code from device code endpoint, calls back to with device code response, and
* polls token endpoint to exchange device code for tokens
* @param request - developer provided CommonDeviceCodeRequest
*/
public async acquireToken(
request: CommonDeviceCodeRequest
): Promise<AuthenticationResult | null> {
const deviceCodeResponse: DeviceCodeResponse = await this.getDeviceCode(
request
);
request.deviceCodeCallback(deviceCodeResponse);
const reqTimestamp = TimeUtils.nowSeconds();
const response: ServerAuthorizationTokenResponse =
await this.acquireTokenWithDeviceCode(request, deviceCodeResponse);
const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
this.cacheManager,
this.cryptoUtils,
this.logger,
this.config.serializableCache,
this.config.persistencePlugin
);
// Validate response. This function throws a server error if an error is returned by the server.
responseHandler.validateTokenResponse(response);
return responseHandler.handleServerTokenResponse(
response,
this.authority,
reqTimestamp,
request
);
}
/**
* Creates device code request and executes http GET
* @param request - developer provided CommonDeviceCodeRequest
*/
private async getDeviceCode(
request: CommonDeviceCodeRequest
): Promise<DeviceCodeResponse> {
const queryParametersString = this.createExtraQueryParameters(request);
const endpoint = UrlString.appendQueryString(
this.authority.deviceCodeEndpoint,
queryParametersString
);
const queryString = this.createQueryString(request);
const headers = this.createTokenRequestHeaders();
const thumbprint: RequestThumbprint = {
clientId: this.config.authOptions.clientId,
authority: request.authority,
scopes: request.scopes,
claims: request.claims,
authenticationScheme: request.authenticationScheme,
resourceRequestMethod: request.resourceRequestMethod,
resourceRequestUri: request.resourceRequestUri,
shrClaims: request.shrClaims,
sshKid: request.sshKid,
};
return this.executePostRequestToDeviceCodeEndpoint(
endpoint,
queryString,
headers,
thumbprint,
request.correlationId
);
}
/**
* Creates query string for the device code request
* @param request - developer provided CommonDeviceCodeRequest
*/
public createExtraQueryParameters(
request: CommonDeviceCodeRequest
): string {
const parameterBuilder = new RequestParameterBuilder();
if (request.extraQueryParameters) {
parameterBuilder.addExtraQueryParameters(
request.extraQueryParameters
);
}
return parameterBuilder.createQueryString();
}
/**
* Executes POST request to device code endpoint
* @param deviceCodeEndpoint - token endpoint
* @param queryString - string to be used in the body of the request
* @param headers - headers for the request
* @param thumbprint - unique request thumbprint
* @param correlationId - correlation id to be used in the request
*/
private async executePostRequestToDeviceCodeEndpoint(
deviceCodeEndpoint: string,
queryString: string,
headers: Record<string, string>,
thumbprint: RequestThumbprint,
correlationId: string
): Promise<DeviceCodeResponse> {
const {
body: {
user_code: userCode,
device_code: deviceCode,
verification_uri: verificationUri,
expires_in: expiresIn,
interval,
message,
},
} = await this.sendPostRequest<ServerDeviceCodeResponse>(
thumbprint,
deviceCodeEndpoint,
{
body: queryString,
headers: headers,
},
correlationId
);
return {
userCode,
deviceCode,
verificationUri,
expiresIn,
interval,
message,
};
}
/**
* Create device code endpoint query parameters and returns string
* @param request - developer provided CommonDeviceCodeRequest
*/
private createQueryString(request: CommonDeviceCodeRequest): string {
const parameterBuilder: RequestParameterBuilder =
new RequestParameterBuilder();
parameterBuilder.addScopes(request.scopes);
parameterBuilder.addClientId(this.config.authOptions.clientId);
if (request.extraQueryParameters) {
parameterBuilder.addExtraQueryParameters(
request.extraQueryParameters
);
}
if (
request.claims ||
(this.config.authOptions.clientCapabilities &&
this.config.authOptions.clientCapabilities.length > 0)
) {
parameterBuilder.addClaims(
request.claims,
this.config.authOptions.clientCapabilities
);
}
return parameterBuilder.createQueryString();
}
/**
* Breaks the polling with specific conditions
* @param deviceCodeExpirationTime - expiration time for the device code request
* @param userSpecifiedTimeout - developer provided timeout, to be compared against deviceCodeExpirationTime
* @param userSpecifiedCancelFlag - boolean indicating the developer would like to cancel the request
*/
private continuePolling(
deviceCodeExpirationTime: number,
userSpecifiedTimeout?: number,
userSpecifiedCancelFlag?: boolean
): boolean {
if (userSpecifiedCancelFlag) {
this.logger.error(
"Token request cancelled by setting DeviceCodeRequest.cancel = true"
);
throw createClientAuthError(
ClientAuthErrorCodes.deviceCodePollingCancelled
);
} else if (
userSpecifiedTimeout &&
userSpecifiedTimeout < deviceCodeExpirationTime &&
TimeUtils.nowSeconds() > userSpecifiedTimeout
) {
this.logger.error(
`User defined timeout for device code polling reached. The timeout was set for ${userSpecifiedTimeout}`
);
throw createClientAuthError(
ClientAuthErrorCodes.userTimeoutReached
);
} else if (TimeUtils.nowSeconds() > deviceCodeExpirationTime) {
if (userSpecifiedTimeout) {
this.logger.verbose(
`User specified timeout ignored as the device code has expired before the timeout elapsed. The user specified timeout was set for ${userSpecifiedTimeout}`
);
}
this.logger.error(
`Device code expired. Expiration time of device code was ${deviceCodeExpirationTime}`
);
throw createClientAuthError(ClientAuthErrorCodes.deviceCodeExpired);
}
return true;
}
/**
* Creates token request with device code response and polls token endpoint at interval set by the device code response
* @param request - developer provided CommonDeviceCodeRequest
* @param deviceCodeResponse - DeviceCodeResponse returned by the security token service device code endpoint
*/
private async acquireTokenWithDeviceCode(
request: CommonDeviceCodeRequest,
deviceCodeResponse: DeviceCodeResponse
): Promise<ServerAuthorizationTokenResponse> {
const queryParametersString = this.createTokenQueryParameters(request);
const endpoint = UrlString.appendQueryString(
this.authority.tokenEndpoint,
queryParametersString
);
const requestBody = this.createTokenRequestBody(
request,
deviceCodeResponse
);
const headers: Record<string, string> =
this.createTokenRequestHeaders();
const userSpecifiedTimeout = request.timeout
? TimeUtils.nowSeconds() + request.timeout
: undefined;
const deviceCodeExpirationTime =
TimeUtils.nowSeconds() + deviceCodeResponse.expiresIn;
const pollingIntervalMilli = deviceCodeResponse.interval * 1000;
/*
* Poll token endpoint while (device code is not expired AND operation has not been cancelled by
* setting CancellationToken.cancel = true). POST request is sent at interval set by pollingIntervalMilli
*/
while (
this.continuePolling(
deviceCodeExpirationTime,
userSpecifiedTimeout,
request.cancel
)
) {
const thumbprint: RequestThumbprint = {
clientId: this.config.authOptions.clientId,
authority: request.authority,
scopes: request.scopes,
claims: request.claims,
authenticationScheme: request.authenticationScheme,
resourceRequestMethod: request.resourceRequestMethod,
resourceRequestUri: request.resourceRequestUri,
shrClaims: request.shrClaims,
sshKid: request.sshKid,
};
const response = await this.executePostToTokenEndpoint(
endpoint,
requestBody,
headers,
thumbprint,
request.correlationId
);
if (response.body && response.body.error) {
// user authorization is pending. Sleep for polling interval and try again
if (response.body.error === Constants.AUTHORIZATION_PENDING) {
this.logger.info(
"Authorization pending. Continue polling."
);
await TimeUtils.delay(pollingIntervalMilli);
} else {
// for any other error, throw
this.logger.info(
"Unexpected error in polling from the server"
);
throw createAuthError(
AuthErrorCodes.postRequestFailed,
response.body.error
);
}
} else {
this.logger.verbose(
"Authorization completed successfully. Polling stopped."
);
return response.body;
}
}
/*
* The above code should've thrown by this point, but to satisfy TypeScript,
* and in the rare case the conditionals in continuePolling() may not catch everything...
*/
this.logger.error("Polling stopped for unknown reasons.");
throw createClientAuthError(
ClientAuthErrorCodes.deviceCodeUnknownError
);
}
/**
* Creates query parameters and converts to string.
* @param request - developer provided CommonDeviceCodeRequest
* @param deviceCodeResponse - DeviceCodeResponse returned by the security token service device code endpoint
*/
private createTokenRequestBody(
request: CommonDeviceCodeRequest,
deviceCodeResponse: DeviceCodeResponse
): string {
const requestParameters: RequestParameterBuilder =
new RequestParameterBuilder();
requestParameters.addScopes(request.scopes);
requestParameters.addClientId(this.config.authOptions.clientId);
requestParameters.addGrantType(GrantType.DEVICE_CODE_GRANT);
requestParameters.addDeviceCode(deviceCodeResponse.deviceCode);
const correlationId =
request.correlationId ||
this.config.cryptoInterface.createNewGuid();
requestParameters.addCorrelationId(correlationId);
requestParameters.addClientInfo();
requestParameters.addLibraryInfo(this.config.libraryInfo);
requestParameters.addApplicationTelemetry(
this.config.telemetry.application
);
requestParameters.addThrottling();
if (this.serverTelemetryManager) {
requestParameters.addServerTelemetry(this.serverTelemetryManager);
}
if (
!StringUtils.isEmptyObj(request.claims) ||
(this.config.authOptions.clientCapabilities &&
this.config.authOptions.clientCapabilities.length > 0)
) {
requestParameters.addClaims(
request.claims,
this.config.authOptions.clientCapabilities
);
}
return requestParameters.createQueryString();
}
}
@@ -0,0 +1,72 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AuthenticationResult,
IAppTokenProvider,
Logger,
} from "@azure/msal-common/node";
import { AuthorizationCodeRequest } from "../request/AuthorizationCodeRequest.js";
import { AuthorizationUrlRequest } from "../request/AuthorizationUrlRequest.js";
import { ClientCredentialRequest } from "../request/ClientCredentialRequest.js";
import { OnBehalfOfRequest } from "../request/OnBehalfOfRequest.js";
import { RefreshTokenRequest } from "../request/RefreshTokenRequest.js";
import { SilentFlowRequest } from "../request/SilentFlowRequest.js";
import { UsernamePasswordRequest } from "../request/UsernamePasswordRequest.js";
import { TokenCache } from "../cache/TokenCache.js";
/**
* Interface for the ConfidentialClientApplication class defining the public API signatures
* @public
*/
export interface IConfidentialClientApplication {
/** Creates the URL of the authorization request */
getAuthCodeUrl(request: AuthorizationUrlRequest): Promise<string>;
/** Acquires a token by exchanging the authorization code received from the first step of OAuth 2.0 Authorization Code Flow */
acquireTokenByCode(
request: AuthorizationCodeRequest
): Promise<AuthenticationResult>;
/** Acquires a token silently when a user specifies the account the token is requested for */
acquireTokenSilent(
request: SilentFlowRequest
): Promise<AuthenticationResult | null>;
/** Acquires a token by exchanging the refresh token provided for a new set of tokens */
acquireTokenByRefreshToken(
request: RefreshTokenRequest
): Promise<AuthenticationResult | null>;
/** Acquires tokens from the authority for the application (not for an end user) */
acquireTokenByClientCredential(
request: ClientCredentialRequest
): Promise<AuthenticationResult | null>;
/** Acquires tokens from the authority for the application */
acquireTokenOnBehalfOf(
request: OnBehalfOfRequest
): Promise<AuthenticationResult | null>;
/** Acquires tokens with password grant by exchanging client applications username and password for credentials */
acquireTokenByUsernamePassword(
request: UsernamePasswordRequest
): Promise<AuthenticationResult | null>;
/** Gets the token cache for the application */
getTokenCache(): TokenCache;
/** Returns the logger instance */
getLogger(): Logger;
/** Replaces the default logger set in configurations with new Logger with new configurations */
setLogger(logger: Logger): void;
/** Clear the cache */
clearCache(): void;
/** This extensibility point is meant for Azure SDK to enhance Managed Identity support */
SetAppTokenProvider(provider: IAppTokenProvider): void;
}
+76
View File
@@ -0,0 +1,76 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AccountInfo,
AuthenticationResult,
Logger,
} from "@azure/msal-common/node";
import { AuthorizationCodeRequest } from "../request/AuthorizationCodeRequest.js";
import { AuthorizationUrlRequest } from "../request/AuthorizationUrlRequest.js";
import { DeviceCodeRequest } from "../request/DeviceCodeRequest.js";
import { RefreshTokenRequest } from "../request/RefreshTokenRequest.js";
import { SilentFlowRequest } from "../request/SilentFlowRequest.js";
import { UsernamePasswordRequest } from "../request/UsernamePasswordRequest.js";
import { TokenCache } from "../cache/TokenCache.js";
import { InteractiveRequest } from "../request/InteractiveRequest.js";
import { SignOutRequest } from "../request/SignOutRequest.js";
/**
* Interface for the PublicClientApplication class defining the public API signatures
* @public
*/
export interface IPublicClientApplication {
/** Creates the URL of the authorization request */
getAuthCodeUrl(request: AuthorizationUrlRequest): Promise<string>;
/** Acquires a token by exchanging the authorization code received from the first step of OAuth 2.0 Authorization Code Flow */
acquireTokenByCode(
request: AuthorizationCodeRequest
): Promise<AuthenticationResult>;
/** Acquires a token interactively */
acquireTokenInteractive(
request: InteractiveRequest
): Promise<AuthenticationResult>;
/** Acquires a token silently when a user specifies the account the token is requested for */
acquireTokenSilent(
request: SilentFlowRequest
): Promise<AuthenticationResult>;
/** Acquires a token by exchanging the refresh token provided for a new set of tokens */
acquireTokenByRefreshToken(
request: RefreshTokenRequest
): Promise<AuthenticationResult | null>;
/** Acquires a token from the authority using OAuth2.0 device code flow */
acquireTokenByDeviceCode(
request: DeviceCodeRequest
): Promise<AuthenticationResult | null>;
/** Acquires tokens with password grant by exchanging client applications username and password for credentials */
acquireTokenByUsernamePassword(
request: UsernamePasswordRequest
): Promise<AuthenticationResult | null>;
/** Gets the token cache for the application */
getTokenCache(): TokenCache;
/** Returns the logger instance */
getLogger(): Logger;
/** Replaces the default logger set in configurations with new Logger with new configurations */
setLogger(logger: Logger): void;
/** Clear the cache */
clearCache(): void;
/** Gets all cached accounts */
getAllAccounts(): Promise<AccountInfo[]>;
/** Removes cache artifacts associated with the given account */
signOut(request: SignOutRequest): Promise<void>;
}
+204
View File
@@ -0,0 +1,204 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AuthOptions,
Authority,
AuthorityOptions,
CacheOutcome,
ClientConfiguration,
Constants,
DEFAULT_CRYPTO_IMPLEMENTATION,
INetworkModule,
Logger,
ProtocolMode,
StaticAuthorityOptions,
AuthenticationResult,
createClientConfigurationError,
ClientConfigurationErrorCodes,
} from "@azure/msal-common/node";
import {
ManagedIdentityConfiguration,
ManagedIdentityNodeConfiguration,
buildManagedIdentityConfiguration,
} from "../config/Configuration.js";
import { version, name } from "../packageMetadata.js";
import { ManagedIdentityRequest } from "../request/ManagedIdentityRequest.js";
import { CryptoProvider } from "../crypto/CryptoProvider.js";
import { ClientCredentialClient } from "./ClientCredentialClient.js";
import { ManagedIdentityClient } from "./ManagedIdentityClient.js";
import { ManagedIdentityRequestParams } from "../request/ManagedIdentityRequestParams.js";
import { NodeStorage } from "../cache/NodeStorage.js";
import {
DEFAULT_AUTHORITY_FOR_MANAGED_IDENTITY,
ManagedIdentitySourceNames,
} from "../utils/Constants.js";
/**
* Class to initialize a managed identity and identify the service
* @public
*/
export class ManagedIdentityApplication {
private config: ManagedIdentityNodeConfiguration;
private logger: Logger;
private static nodeStorage?: NodeStorage;
private networkClient: INetworkModule;
private cryptoProvider: CryptoProvider;
// authority needs to be faked to re-use existing functionality in msal-common: caching in responseHandler, etc.
private fakeAuthority: Authority;
// the ClientCredentialClient class needs to be faked to call it's getCachedAuthenticationResult method
private fakeClientCredentialClient: ClientCredentialClient;
private managedIdentityClient: ManagedIdentityClient;
constructor(configuration?: ManagedIdentityConfiguration) {
// undefined config means the managed identity is system-assigned
this.config = buildManagedIdentityConfiguration(configuration || {});
this.logger = new Logger(
this.config.system.loggerOptions,
name,
version
);
const fakeStatusAuthorityOptions: StaticAuthorityOptions = {
canonicalAuthority: Constants.DEFAULT_AUTHORITY,
};
if (!ManagedIdentityApplication.nodeStorage) {
ManagedIdentityApplication.nodeStorage = new NodeStorage(
this.logger,
this.config.managedIdentityId.id,
DEFAULT_CRYPTO_IMPLEMENTATION,
fakeStatusAuthorityOptions
);
}
this.networkClient = this.config.system.networkClient;
this.cryptoProvider = new CryptoProvider();
const fakeAuthorityOptions: AuthorityOptions = {
protocolMode: ProtocolMode.AAD,
knownAuthorities: [DEFAULT_AUTHORITY_FOR_MANAGED_IDENTITY],
cloudDiscoveryMetadata: "",
authorityMetadata: "",
};
this.fakeAuthority = new Authority(
DEFAULT_AUTHORITY_FOR_MANAGED_IDENTITY,
this.networkClient,
ManagedIdentityApplication.nodeStorage as NodeStorage,
fakeAuthorityOptions,
this.logger,
this.cryptoProvider.createNewGuid(), // correlationID
undefined,
true
);
this.fakeClientCredentialClient = new ClientCredentialClient({
authOptions: {
clientId: this.config.managedIdentityId.id,
authority: this.fakeAuthority,
} as AuthOptions,
} as ClientConfiguration);
this.managedIdentityClient = new ManagedIdentityClient(
this.logger,
ManagedIdentityApplication.nodeStorage as NodeStorage,
this.networkClient,
this.cryptoProvider
);
}
/**
* Acquire an access token from the cache or the managed identity
* @param managedIdentityRequest - the ManagedIdentityRequestParams object passed in by the developer
* @returns the access token
*/
public async acquireToken(
managedIdentityRequestParams: ManagedIdentityRequestParams
): Promise<AuthenticationResult> {
if (!managedIdentityRequestParams.resource) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.urlEmptyError
);
}
const managedIdentityRequest: ManagedIdentityRequest = {
forceRefresh: managedIdentityRequestParams.forceRefresh,
resource: managedIdentityRequestParams.resource.replace(
"/.default",
""
),
scopes: [
managedIdentityRequestParams.resource.replace("/.default", ""),
],
authority: this.fakeAuthority.canonicalAuthority,
correlationId: this.cryptoProvider.createNewGuid(),
};
if (
managedIdentityRequestParams.claims ||
managedIdentityRequest.forceRefresh
) {
// make a network call to the managed identity source
return this.managedIdentityClient.sendManagedIdentityTokenRequest(
managedIdentityRequest,
this.config.managedIdentityId,
this.fakeAuthority
);
}
const [cachedAuthenticationResult, lastCacheOutcome] =
await this.fakeClientCredentialClient.getCachedAuthenticationResult(
managedIdentityRequest,
this.config,
this.cryptoProvider,
this.fakeAuthority,
ManagedIdentityApplication.nodeStorage as NodeStorage
);
if (cachedAuthenticationResult) {
// if the token is not expired but must be refreshed; get a new one in the background
if (lastCacheOutcome === CacheOutcome.PROACTIVELY_REFRESHED) {
this.logger.info(
"ClientCredentialClient:getCachedAuthenticationResult - Cached access token's refreshOn property has been exceeded'. It's not expired, but must be refreshed."
);
// make a network call to the managed identity source; refresh the access token in the background
const refreshAccessToken = true;
await this.managedIdentityClient.sendManagedIdentityTokenRequest(
managedIdentityRequest,
this.config.managedIdentityId,
this.fakeAuthority,
refreshAccessToken
);
}
return cachedAuthenticationResult;
} else {
// make a network call to the managed identity source
return this.managedIdentityClient.sendManagedIdentityTokenRequest(
managedIdentityRequest,
this.config.managedIdentityId,
this.fakeAuthority
);
}
}
/**
* Determine the Managed Identity Source based on available environment variables. This API is consumed by Azure Identity SDK.
* @returns ManagedIdentitySourceNames - The Managed Identity source's name
*/
public getManagedIdentitySource(): ManagedIdentitySourceNames {
return (
ManagedIdentityClient.sourceName ||
this.managedIdentityClient.getManagedIdentitySource()
);
}
}
+162
View File
@@ -0,0 +1,162 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
Authority,
INetworkModule,
Logger,
AuthenticationResult,
} from "@azure/msal-common/node";
import { AppService } from "./ManagedIdentitySources/AppService.js";
import { AzureArc } from "./ManagedIdentitySources/AzureArc.js";
import { CloudShell } from "./ManagedIdentitySources/CloudShell.js";
import { Imds } from "./ManagedIdentitySources/Imds.js";
import { ServiceFabric } from "./ManagedIdentitySources/ServiceFabric.js";
import { CryptoProvider } from "../crypto/CryptoProvider.js";
import {
ManagedIdentityErrorCodes,
createManagedIdentityError,
} from "../error/ManagedIdentityError.js";
import { ManagedIdentityRequest } from "../request/ManagedIdentityRequest.js";
import { ManagedIdentityId } from "../config/ManagedIdentityId.js";
import { NodeStorage } from "../cache/NodeStorage.js";
import { BaseManagedIdentitySource } from "./ManagedIdentitySources/BaseManagedIdentitySource.js";
import { ManagedIdentitySourceNames } from "../utils/Constants.js";
/*
* Class to initialize a managed identity and identify the service.
* Original source of code: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/ManagedIdentityClient.cs
*/
export class ManagedIdentityClient {
private logger: Logger;
private nodeStorage: NodeStorage;
private networkClient: INetworkModule;
private cryptoProvider: CryptoProvider;
private static identitySource?: BaseManagedIdentitySource;
public static sourceName?: ManagedIdentitySourceNames;
constructor(
logger: Logger,
nodeStorage: NodeStorage,
networkClient: INetworkModule,
cryptoProvider: CryptoProvider
) {
this.logger = logger;
this.nodeStorage = nodeStorage;
this.networkClient = networkClient;
this.cryptoProvider = cryptoProvider;
}
public async sendManagedIdentityTokenRequest(
managedIdentityRequest: ManagedIdentityRequest,
managedIdentityId: ManagedIdentityId,
fakeAuthority: Authority,
refreshAccessToken?: boolean
): Promise<AuthenticationResult> {
if (!ManagedIdentityClient.identitySource) {
ManagedIdentityClient.identitySource =
this.selectManagedIdentitySource(
this.logger,
this.nodeStorage,
this.networkClient,
this.cryptoProvider,
managedIdentityId
);
}
return ManagedIdentityClient.identitySource.acquireTokenWithManagedIdentity(
managedIdentityRequest,
managedIdentityId,
fakeAuthority,
refreshAccessToken
);
}
private allEnvironmentVariablesAreDefined(
environmentVariables: Array<string | undefined>
): boolean {
return Object.values(environmentVariables).every(
(environmentVariable) => {
return environmentVariable !== undefined;
}
);
}
/**
* Determine the Managed Identity Source based on available environment variables. This API is consumed by ManagedIdentityApplication's getManagedIdentitySource.
* @returns ManagedIdentitySourceNames - The Managed Identity source's name
*/
public getManagedIdentitySource(): ManagedIdentitySourceNames {
ManagedIdentityClient.sourceName =
this.allEnvironmentVariablesAreDefined(
ServiceFabric.getEnvironmentVariables()
)
? ManagedIdentitySourceNames.SERVICE_FABRIC
: this.allEnvironmentVariablesAreDefined(
AppService.getEnvironmentVariables()
)
? ManagedIdentitySourceNames.APP_SERVICE
: this.allEnvironmentVariablesAreDefined(
CloudShell.getEnvironmentVariables()
)
? ManagedIdentitySourceNames.CLOUD_SHELL
: this.allEnvironmentVariablesAreDefined(
AzureArc.getEnvironmentVariables()
)
? ManagedIdentitySourceNames.AZURE_ARC
: ManagedIdentitySourceNames.DEFAULT_TO_IMDS;
return ManagedIdentityClient.sourceName;
}
/**
* Tries to create a managed identity source for all sources
* @returns the managed identity Source
*/
private selectManagedIdentitySource(
logger: Logger,
nodeStorage: NodeStorage,
networkClient: INetworkModule,
cryptoProvider: CryptoProvider,
managedIdentityId: ManagedIdentityId
): BaseManagedIdentitySource {
const source =
ServiceFabric.tryCreate(
logger,
nodeStorage,
networkClient,
cryptoProvider,
managedIdentityId
) ||
AppService.tryCreate(
logger,
nodeStorage,
networkClient,
cryptoProvider
) ||
CloudShell.tryCreate(
logger,
nodeStorage,
networkClient,
cryptoProvider,
managedIdentityId
) ||
AzureArc.tryCreate(
logger,
nodeStorage,
networkClient,
cryptoProvider,
managedIdentityId
) ||
Imds.tryCreate(logger, nodeStorage, networkClient, cryptoProvider);
if (!source) {
throw createManagedIdentityError(
ManagedIdentityErrorCodes.unableToCreateSource
);
}
return source;
}
}
@@ -0,0 +1,129 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { INetworkModule, Logger } from "@azure/msal-common/node";
import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js";
import {
HttpMethod,
APP_SERVICE_SECRET_HEADER_NAME,
API_VERSION_QUERY_PARAMETER_NAME,
RESOURCE_BODY_OR_QUERY_PARAMETER_NAME,
ManagedIdentityEnvironmentVariableNames,
ManagedIdentitySourceNames,
ManagedIdentityIdType,
} from "../../utils/Constants.js";
import { CryptoProvider } from "../../crypto/CryptoProvider.js";
import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js";
import { ManagedIdentityId } from "../../config/ManagedIdentityId.js";
import { NodeStorage } from "../../cache/NodeStorage.js";
// MSI Constants. Docs for MSI are available here https://docs.microsoft.com/azure/app-service/overview-managed-identity
const APP_SERVICE_MSI_API_VERSION: string = "2019-08-01";
/**
* Original source of code: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/AppServiceManagedIdentitySource.cs
*/
export class AppService extends BaseManagedIdentitySource {
private identityEndpoint: string;
private identityHeader: string;
constructor(
logger: Logger,
nodeStorage: NodeStorage,
networkClient: INetworkModule,
cryptoProvider: CryptoProvider,
identityEndpoint: string,
identityHeader: string
) {
super(logger, nodeStorage, networkClient, cryptoProvider);
this.identityEndpoint = identityEndpoint;
this.identityHeader = identityHeader;
}
public static getEnvironmentVariables(): Array<string | undefined> {
const identityEndpoint: string | undefined =
process.env[
ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT
];
const identityHeader: string | undefined =
process.env[
ManagedIdentityEnvironmentVariableNames.IDENTITY_HEADER
];
return [identityEndpoint, identityHeader];
}
public static tryCreate(
logger: Logger,
nodeStorage: NodeStorage,
networkClient: INetworkModule,
cryptoProvider: CryptoProvider
): AppService | null {
const [identityEndpoint, identityHeader] =
AppService.getEnvironmentVariables();
// if either of the identity endpoint or identity header variables are undefined, this MSI provider is unavailable.
if (!identityEndpoint || !identityHeader) {
logger.info(
`[Managed Identity] ${ManagedIdentitySourceNames.APP_SERVICE} managed identity is unavailable because one or both of the '${ManagedIdentityEnvironmentVariableNames.IDENTITY_HEADER}' and '${ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT}' environment variables are not defined.`
);
return null;
}
const validatedIdentityEndpoint: string =
AppService.getValidatedEnvVariableUrlString(
ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT,
identityEndpoint,
ManagedIdentitySourceNames.APP_SERVICE,
logger
);
logger.info(
`[Managed Identity] Environment variables validation passed for ${ManagedIdentitySourceNames.APP_SERVICE} managed identity. Endpoint URI: ${validatedIdentityEndpoint}. Creating ${ManagedIdentitySourceNames.APP_SERVICE} managed identity.`
);
return new AppService(
logger,
nodeStorage,
networkClient,
cryptoProvider,
identityEndpoint,
identityHeader
);
}
public createRequest(
resource: string,
managedIdentityId: ManagedIdentityId
): ManagedIdentityRequestParameters {
const request: ManagedIdentityRequestParameters =
new ManagedIdentityRequestParameters(
HttpMethod.GET,
this.identityEndpoint
);
request.headers[APP_SERVICE_SECRET_HEADER_NAME] = this.identityHeader;
request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] =
APP_SERVICE_MSI_API_VERSION;
request.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] =
resource;
if (
managedIdentityId.idType !== ManagedIdentityIdType.SYSTEM_ASSIGNED
) {
request.queryParameters[
this.getManagedIdentityUserAssignedIdQueryParameterKey(
managedIdentityId.idType
)
] = managedIdentityId.id;
}
// bodyParameters calculated in BaseManagedIdentity.acquireTokenWithManagedIdentity
return request;
}
}
@@ -0,0 +1,318 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AuthError,
ClientAuthErrorCodes,
createClientAuthError,
HttpStatus,
INetworkModule,
NetworkResponse,
NetworkRequestOptions,
Logger,
ServerAuthorizationTokenResponse,
} from "@azure/msal-common/node";
import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js";
import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js";
import { CryptoProvider } from "../../crypto/CryptoProvider.js";
import {
ManagedIdentityErrorCodes,
createManagedIdentityError,
} from "../../error/ManagedIdentityError.js";
import {
API_VERSION_QUERY_PARAMETER_NAME,
AUTHORIZATION_HEADER_NAME,
AZURE_ARC_SECRET_FILE_MAX_SIZE_BYTES,
HttpMethod,
METADATA_HEADER_NAME,
ManagedIdentityEnvironmentVariableNames,
ManagedIdentityIdType,
ManagedIdentitySourceNames,
RESOURCE_BODY_OR_QUERY_PARAMETER_NAME,
} from "../../utils/Constants.js";
import { NodeStorage } from "../../cache/NodeStorage.js";
import {
accessSync,
constants as fsConstants,
readFileSync,
statSync,
} from "fs";
import { ManagedIdentityTokenResponse } from "../../response/ManagedIdentityTokenResponse.js";
import { ManagedIdentityId } from "../../config/ManagedIdentityId.js";
import path from "path";
export const ARC_API_VERSION: string = "2019-11-01";
export const DEFAULT_AZURE_ARC_IDENTITY_ENDPOINT: string =
"http://127.0.0.1:40342/metadata/identity/oauth2/token";
const HIMDS_EXECUTABLE_HELPER_STRING = "N/A: himds executable exists";
type FilePathMap = {
win32: string;
linux: string;
};
export const SUPPORTED_AZURE_ARC_PLATFORMS: FilePathMap = {
win32: `${process.env["ProgramData"]}\\AzureConnectedMachineAgent\\Tokens\\`,
linux: "/var/opt/azcmagent/tokens/",
};
export const AZURE_ARC_FILE_DETECTION: FilePathMap = {
win32: `${process.env["ProgramFiles"]}\\AzureConnectedMachineAgent\\himds.exe`,
linux: "/opt/azcmagent/bin/himds",
};
/**
* Original source of code: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/AzureArcManagedIdentitySource.cs
*/
export class AzureArc extends BaseManagedIdentitySource {
private identityEndpoint: string;
constructor(
logger: Logger,
nodeStorage: NodeStorage,
networkClient: INetworkModule,
cryptoProvider: CryptoProvider,
identityEndpoint: string
) {
super(logger, nodeStorage, networkClient, cryptoProvider);
this.identityEndpoint = identityEndpoint;
}
public static getEnvironmentVariables(): Array<string | undefined> {
let identityEndpoint: string | undefined =
process.env[
ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT
];
let imdsEndpoint: string | undefined =
process.env[ManagedIdentityEnvironmentVariableNames.IMDS_ENDPOINT];
// if either of the identity or imds endpoints are undefined, check if the himds executable exists
if (!identityEndpoint || !imdsEndpoint) {
// get the expected Windows or Linux file path of the himds executable
const fileDetectionPath: string =
AZURE_ARC_FILE_DETECTION[process.platform as keyof FilePathMap];
try {
/*
* check if the himds executable exists and its permissions allow it to be read
* returns undefined if true, throws an error otherwise
*/
accessSync(
fileDetectionPath,
fsConstants.F_OK | fsConstants.R_OK
);
identityEndpoint = DEFAULT_AZURE_ARC_IDENTITY_ENDPOINT;
imdsEndpoint = HIMDS_EXECUTABLE_HELPER_STRING;
} catch (err) {
/*
* do nothing
* accessSync returns undefined on success, and throws an error on failure
*/
}
}
return [identityEndpoint, imdsEndpoint];
}
public static tryCreate(
logger: Logger,
nodeStorage: NodeStorage,
networkClient: INetworkModule,
cryptoProvider: CryptoProvider,
managedIdentityId: ManagedIdentityId
): AzureArc | null {
const [identityEndpoint, imdsEndpoint] =
AzureArc.getEnvironmentVariables();
// if either of the identity or imds endpoints are undefined (even after himds file detection)
if (!identityEndpoint || !imdsEndpoint) {
logger.info(
`[Managed Identity] ${ManagedIdentitySourceNames.AZURE_ARC} managed identity is unavailable through environment variables because one or both of '${ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT}' and '${ManagedIdentityEnvironmentVariableNames.IMDS_ENDPOINT}' are not defined. ${ManagedIdentitySourceNames.AZURE_ARC} managed identity is also unavailable through file detection.`
);
return null;
}
// check if the imds endpoint is set to the default for file detection
if (imdsEndpoint === HIMDS_EXECUTABLE_HELPER_STRING) {
logger.info(
`[Managed Identity] ${ManagedIdentitySourceNames.AZURE_ARC} managed identity is available through file detection. Defaulting to known ${ManagedIdentitySourceNames.AZURE_ARC} endpoint: ${DEFAULT_AZURE_ARC_IDENTITY_ENDPOINT}. Creating ${ManagedIdentitySourceNames.AZURE_ARC} managed identity.`
);
} else {
// otherwise, both the identity and imds endpoints are defined without file detection; validate them
const validatedIdentityEndpoint: string =
AzureArc.getValidatedEnvVariableUrlString(
ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT,
identityEndpoint,
ManagedIdentitySourceNames.AZURE_ARC,
logger
);
// remove trailing slash
validatedIdentityEndpoint.endsWith("/")
? validatedIdentityEndpoint.slice(0, -1)
: validatedIdentityEndpoint;
AzureArc.getValidatedEnvVariableUrlString(
ManagedIdentityEnvironmentVariableNames.IMDS_ENDPOINT,
imdsEndpoint,
ManagedIdentitySourceNames.AZURE_ARC,
logger
);
logger.info(
`[Managed Identity] Environment variables validation passed for ${ManagedIdentitySourceNames.AZURE_ARC} managed identity. Endpoint URI: ${validatedIdentityEndpoint}. Creating ${ManagedIdentitySourceNames.AZURE_ARC} managed identity.`
);
}
if (
managedIdentityId.idType !== ManagedIdentityIdType.SYSTEM_ASSIGNED
) {
throw createManagedIdentityError(
ManagedIdentityErrorCodes.unableToCreateAzureArc
);
}
return new AzureArc(
logger,
nodeStorage,
networkClient,
cryptoProvider,
identityEndpoint
);
}
public createRequest(resource: string): ManagedIdentityRequestParameters {
const request: ManagedIdentityRequestParameters =
new ManagedIdentityRequestParameters(
HttpMethod.GET,
this.identityEndpoint.replace("localhost", "127.0.0.1")
);
request.headers[METADATA_HEADER_NAME] = "true";
request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] =
ARC_API_VERSION;
request.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] =
resource;
// bodyParameters calculated in BaseManagedIdentity.acquireTokenWithManagedIdentity
return request;
}
public async getServerTokenResponseAsync(
originalResponse: NetworkResponse<ManagedIdentityTokenResponse>,
networkClient: INetworkModule,
networkRequest: ManagedIdentityRequestParameters,
networkRequestOptions: NetworkRequestOptions
): Promise<ServerAuthorizationTokenResponse> {
let retryResponse:
| NetworkResponse<ManagedIdentityTokenResponse>
| undefined;
if (originalResponse.status === HttpStatus.UNAUTHORIZED) {
const wwwAuthHeader: string =
originalResponse.headers["www-authenticate"];
if (!wwwAuthHeader) {
throw createManagedIdentityError(
ManagedIdentityErrorCodes.wwwAuthenticateHeaderMissing
);
}
if (!wwwAuthHeader.includes("Basic realm=")) {
throw createManagedIdentityError(
ManagedIdentityErrorCodes.wwwAuthenticateHeaderUnsupportedFormat
);
}
const secretFilePath = wwwAuthHeader.split("Basic realm=")[1];
// throw an error if the managed identity application is not being run on Windows or Linux
if (
!SUPPORTED_AZURE_ARC_PLATFORMS.hasOwnProperty(process.platform)
) {
throw createManagedIdentityError(
ManagedIdentityErrorCodes.platformNotSupported
);
}
// get the expected Windows or Linux file path
const expectedSecretFilePath: string =
SUPPORTED_AZURE_ARC_PLATFORMS[
process.platform as keyof FilePathMap
];
// throw an error if the file in the file path is not a .key file
const fileName: string = path.basename(secretFilePath);
if (!fileName.endsWith(".key")) {
throw createManagedIdentityError(
ManagedIdentityErrorCodes.invalidFileExtension
);
}
/*
* throw an error if the file path from the www-authenticate header does not match the
* expected file path for the platform (Windows or Linux) the managed identity application
* is running on
*/
if (expectedSecretFilePath + fileName !== secretFilePath) {
throw createManagedIdentityError(
ManagedIdentityErrorCodes.invalidFilePath
);
}
let secretFileSize;
// attempt to get the secret file's size, in bytes
try {
secretFileSize = await statSync(secretFilePath).size;
} catch (e) {
throw createManagedIdentityError(
ManagedIdentityErrorCodes.unableToReadSecretFile
);
}
// throw an error if the secret file's size is greater than 4096 bytes
if (secretFileSize > AZURE_ARC_SECRET_FILE_MAX_SIZE_BYTES) {
throw createManagedIdentityError(
ManagedIdentityErrorCodes.invalidSecret
);
}
// attempt to read the contents of the secret file
let secret;
try {
secret = readFileSync(secretFilePath, "utf-8");
} catch (e) {
throw createManagedIdentityError(
ManagedIdentityErrorCodes.unableToReadSecretFile
);
}
const authHeaderValue = `Basic ${secret}`;
this.logger.info(
`[Managed Identity] Adding authorization header to the request.`
);
networkRequest.headers[AUTHORIZATION_HEADER_NAME] = authHeaderValue;
try {
retryResponse =
await networkClient.sendGetRequestAsync<ManagedIdentityTokenResponse>(
networkRequest.computeUri(),
networkRequestOptions
);
} catch (error) {
if (error instanceof AuthError) {
throw error;
} else {
throw createClientAuthError(
ClientAuthErrorCodes.networkError
);
}
}
}
return this.getServerTokenResponse(retryResponse || originalResponse);
}
}
@@ -0,0 +1,252 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AuthError,
Authority,
ClientAuthErrorCodes,
Constants,
HeaderNames,
INetworkModule,
Logger,
NetworkRequestOptions,
NetworkResponse,
ResponseHandler,
ServerAuthorizationTokenResponse,
TimeUtils,
createClientAuthError,
AuthenticationResult,
UrlString,
} from "@azure/msal-common/node";
import { ManagedIdentityId } from "../../config/ManagedIdentityId.js";
import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js";
import { CryptoProvider } from "../../crypto/CryptoProvider.js";
import { ManagedIdentityRequest } from "../../request/ManagedIdentityRequest.js";
import { HttpMethod, ManagedIdentityIdType } from "../../utils/Constants.js";
import { ManagedIdentityTokenResponse } from "../../response/ManagedIdentityTokenResponse.js";
import { NodeStorage } from "../../cache/NodeStorage.js";
import {
ManagedIdentityErrorCodes,
createManagedIdentityError,
} from "../../error/ManagedIdentityError.js";
/**
* Managed Identity User Assigned Id Query Parameter Names
*/
export const ManagedIdentityUserAssignedIdQueryParameterNames = {
MANAGED_IDENTITY_CLIENT_ID: "client_id",
MANAGED_IDENTITY_OBJECT_ID: "object_id",
MANAGED_IDENTITY_RESOURCE_ID: "mi_res_id",
} as const;
export type ManagedIdentityUserAssignedIdQueryParameterNames =
(typeof ManagedIdentityUserAssignedIdQueryParameterNames)[keyof typeof ManagedIdentityUserAssignedIdQueryParameterNames];
export abstract class BaseManagedIdentitySource {
protected logger: Logger;
private nodeStorage: NodeStorage;
private networkClient: INetworkModule;
private cryptoProvider: CryptoProvider;
constructor(
logger: Logger,
nodeStorage: NodeStorage,
networkClient: INetworkModule,
cryptoProvider: CryptoProvider
) {
this.logger = logger;
this.nodeStorage = nodeStorage;
this.networkClient = networkClient;
this.cryptoProvider = cryptoProvider;
}
abstract createRequest(
request: string,
managedIdentityId: ManagedIdentityId
): ManagedIdentityRequestParameters;
public async getServerTokenResponseAsync(
response: NetworkResponse<ManagedIdentityTokenResponse>,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_networkClient: INetworkModule,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_networkRequest: ManagedIdentityRequestParameters,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_networkRequestOptions: NetworkRequestOptions
): Promise<ServerAuthorizationTokenResponse> {
return this.getServerTokenResponse(response);
}
public getServerTokenResponse(
response: NetworkResponse<ManagedIdentityTokenResponse>
): ServerAuthorizationTokenResponse {
let refreshIn, expiresIn: number | undefined;
if (response.body.expires_on) {
expiresIn = response.body.expires_on - TimeUtils.nowSeconds();
// compute refresh_in as 1/2 of expires_in, but only if expires_in > 2h
if (expiresIn > 2 * 3600) {
refreshIn = expiresIn / 2;
}
}
const serverTokenResponse: ServerAuthorizationTokenResponse = {
status: response.status,
// success
access_token: response.body.access_token,
expires_in: expiresIn,
scope: response.body.resource,
token_type: response.body.token_type,
refresh_in: refreshIn,
// error
correlation_id:
response.body.correlation_id || response.body.correlationId,
error:
typeof response.body.error === "string"
? response.body.error
: response.body.error?.code,
error_description:
response.body.message ||
(typeof response.body.error === "string"
? response.body.error_description
: response.body.error?.message),
error_codes: response.body.error_codes,
timestamp: response.body.timestamp,
trace_id: response.body.trace_id,
};
return serverTokenResponse;
}
public async acquireTokenWithManagedIdentity(
managedIdentityRequest: ManagedIdentityRequest,
managedIdentityId: ManagedIdentityId,
fakeAuthority: Authority,
refreshAccessToken?: boolean
): Promise<AuthenticationResult> {
const networkRequest: ManagedIdentityRequestParameters =
this.createRequest(
managedIdentityRequest.resource,
managedIdentityId
);
const headers: Record<string, string> = networkRequest.headers;
headers[HeaderNames.CONTENT_TYPE] = Constants.URL_FORM_CONTENT_TYPE;
const networkRequestOptions: NetworkRequestOptions = { headers };
if (Object.keys(networkRequest.bodyParameters).length) {
networkRequestOptions.body =
networkRequest.computeParametersBodyString();
}
const reqTimestamp = TimeUtils.nowSeconds();
let response: NetworkResponse<ManagedIdentityTokenResponse>;
try {
// Sources that send POST requests: Cloud Shell
if (networkRequest.httpMethod === HttpMethod.POST) {
response =
await this.networkClient.sendPostRequestAsync<ManagedIdentityTokenResponse>(
networkRequest.computeUri(),
networkRequestOptions
);
// Sources that send GET requests: App Service, Azure Arc, IMDS, Service Fabric
} else {
response =
await this.networkClient.sendGetRequestAsync<ManagedIdentityTokenResponse>(
networkRequest.computeUri(),
networkRequestOptions
);
}
} catch (error) {
if (error instanceof AuthError) {
throw error;
} else {
throw createClientAuthError(ClientAuthErrorCodes.networkError);
}
}
const responseHandler = new ResponseHandler(
managedIdentityId.id,
this.nodeStorage,
this.cryptoProvider,
this.logger,
null,
null
);
const serverTokenResponse: ServerAuthorizationTokenResponse =
await this.getServerTokenResponseAsync(
response,
this.networkClient,
networkRequest,
networkRequestOptions
);
responseHandler.validateTokenResponse(
serverTokenResponse,
refreshAccessToken
);
// caches the token
return responseHandler.handleServerTokenResponse(
serverTokenResponse,
fakeAuthority,
reqTimestamp,
managedIdentityRequest
);
}
public getManagedIdentityUserAssignedIdQueryParameterKey(
managedIdentityIdType: ManagedIdentityIdType
): string {
switch (managedIdentityIdType) {
case ManagedIdentityIdType.USER_ASSIGNED_CLIENT_ID:
this.logger.info(
"[Managed Identity] Adding user assigned client id to the request."
);
return ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID;
case ManagedIdentityIdType.USER_ASSIGNED_RESOURCE_ID:
this.logger.info(
"[Managed Identity] Adding user assigned resource id to the request."
);
return ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_RESOURCE_ID;
case ManagedIdentityIdType.USER_ASSIGNED_OBJECT_ID:
this.logger.info(
"[Managed Identity] Adding user assigned object id to the request."
);
return ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_OBJECT_ID;
default:
throw createManagedIdentityError(
ManagedIdentityErrorCodes.invalidManagedIdentityIdType
);
}
}
public static getValidatedEnvVariableUrlString = (
envVariableStringName: string,
envVariable: string,
sourceName: string,
logger: Logger
): string => {
try {
return new UrlString(envVariable).urlString;
} catch (error) {
logger.info(
`[Managed Identity] ${sourceName} managed identity is unavailable because the '${envVariableStringName}' environment variable is malformed.`
);
throw createManagedIdentityError(
ManagedIdentityErrorCodes
.MsiEnvironmentVariableUrlMalformedErrorCodes[
envVariableStringName
]
);
}
};
}
@@ -0,0 +1,110 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { INetworkModule, Logger } from "@azure/msal-common/node";
import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js";
import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js";
import { NodeStorage } from "../../cache/NodeStorage.js";
import { CryptoProvider } from "../../crypto/CryptoProvider.js";
import {
HttpMethod,
METADATA_HEADER_NAME,
ManagedIdentityEnvironmentVariableNames,
ManagedIdentityIdType,
ManagedIdentitySourceNames,
RESOURCE_BODY_OR_QUERY_PARAMETER_NAME,
} from "../../utils/Constants.js";
import {
ManagedIdentityErrorCodes,
createManagedIdentityError,
} from "../../error/ManagedIdentityError.js";
import { ManagedIdentityId } from "../../config/ManagedIdentityId.js";
/**
* Original source of code: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/CloudShellManagedIdentitySource.cs
*/
export class CloudShell extends BaseManagedIdentitySource {
private msiEndpoint: string;
constructor(
logger: Logger,
nodeStorage: NodeStorage,
networkClient: INetworkModule,
cryptoProvider: CryptoProvider,
msiEndpoint: string
) {
super(logger, nodeStorage, networkClient, cryptoProvider);
this.msiEndpoint = msiEndpoint;
}
public static getEnvironmentVariables(): Array<string | undefined> {
const msiEndpoint: string | undefined =
process.env[ManagedIdentityEnvironmentVariableNames.MSI_ENDPOINT];
return [msiEndpoint];
}
public static tryCreate(
logger: Logger,
nodeStorage: NodeStorage,
networkClient: INetworkModule,
cryptoProvider: CryptoProvider,
managedIdentityId: ManagedIdentityId
): CloudShell | null {
const [msiEndpoint] = CloudShell.getEnvironmentVariables();
// if the msi endpoint environment variable is undefined, this MSI provider is unavailable.
if (!msiEndpoint) {
logger.info(
`[Managed Identity] ${ManagedIdentitySourceNames.CLOUD_SHELL} managed identity is unavailable because the '${ManagedIdentityEnvironmentVariableNames.MSI_ENDPOINT} environment variable is not defined.`
);
return null;
}
const validatedMsiEndpoint: string =
CloudShell.getValidatedEnvVariableUrlString(
ManagedIdentityEnvironmentVariableNames.MSI_ENDPOINT,
msiEndpoint,
ManagedIdentitySourceNames.CLOUD_SHELL,
logger
);
logger.info(
`[Managed Identity] Environment variable validation passed for ${ManagedIdentitySourceNames.CLOUD_SHELL} managed identity. Endpoint URI: ${validatedMsiEndpoint}. Creating ${ManagedIdentitySourceNames.CLOUD_SHELL} managed identity.`
);
if (
managedIdentityId.idType !== ManagedIdentityIdType.SYSTEM_ASSIGNED
) {
throw createManagedIdentityError(
ManagedIdentityErrorCodes.unableToCreateCloudShell
);
}
return new CloudShell(
logger,
nodeStorage,
networkClient,
cryptoProvider,
msiEndpoint
);
}
public createRequest(resource: string): ManagedIdentityRequestParameters {
const request: ManagedIdentityRequestParameters =
new ManagedIdentityRequestParameters(
HttpMethod.POST,
this.msiEndpoint
);
request.headers[METADATA_HEADER_NAME] = "true";
request.bodyParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] =
resource;
return request;
}
}
+126
View File
@@ -0,0 +1,126 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { INetworkModule, Logger } from "@azure/msal-common/node";
import { ManagedIdentityId } from "../../config/ManagedIdentityId.js";
import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js";
import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js";
import { CryptoProvider } from "../../crypto/CryptoProvider.js";
import {
API_VERSION_QUERY_PARAMETER_NAME,
HttpMethod,
METADATA_HEADER_NAME,
ManagedIdentityEnvironmentVariableNames,
ManagedIdentityIdType,
ManagedIdentitySourceNames,
RESOURCE_BODY_OR_QUERY_PARAMETER_NAME,
} from "../../utils/Constants.js";
import { NodeStorage } from "../../cache/NodeStorage.js";
// IMDS constants. Docs for IMDS are available here https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
const IMDS_TOKEN_PATH: string = "/metadata/identity/oauth2/token";
const DEFAULT_IMDS_ENDPOINT: string = `http://169.254.169.254${IMDS_TOKEN_PATH}`;
const IMDS_API_VERSION: string = "2018-02-01";
// Original source of code: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/ImdsManagedIdentitySource.cs
export class Imds extends BaseManagedIdentitySource {
private identityEndpoint: string;
constructor(
logger: Logger,
nodeStorage: NodeStorage,
networkClient: INetworkModule,
cryptoProvider: CryptoProvider,
identityEndpoint: string
) {
super(logger, nodeStorage, networkClient, cryptoProvider);
this.identityEndpoint = identityEndpoint;
}
public static tryCreate(
logger: Logger,
nodeStorage: NodeStorage,
networkClient: INetworkModule,
cryptoProvider: CryptoProvider
): Imds {
let validatedIdentityEndpoint: string;
if (
process.env[
ManagedIdentityEnvironmentVariableNames
.AZURE_POD_IDENTITY_AUTHORITY_HOST
]
) {
logger.info(
`[Managed Identity] Environment variable ${
ManagedIdentityEnvironmentVariableNames.AZURE_POD_IDENTITY_AUTHORITY_HOST
} for ${ManagedIdentitySourceNames.IMDS} returned endpoint: ${
process.env[
ManagedIdentityEnvironmentVariableNames
.AZURE_POD_IDENTITY_AUTHORITY_HOST
]
}`
);
validatedIdentityEndpoint = Imds.getValidatedEnvVariableUrlString(
ManagedIdentityEnvironmentVariableNames.AZURE_POD_IDENTITY_AUTHORITY_HOST,
`${
process.env[
ManagedIdentityEnvironmentVariableNames
.AZURE_POD_IDENTITY_AUTHORITY_HOST
]
}${IMDS_TOKEN_PATH}`,
ManagedIdentitySourceNames.IMDS,
logger
);
} else {
logger.info(
`[Managed Identity] Unable to find ${ManagedIdentityEnvironmentVariableNames.AZURE_POD_IDENTITY_AUTHORITY_HOST} environment variable for ${ManagedIdentitySourceNames.IMDS}, using the default endpoint.`
);
validatedIdentityEndpoint = DEFAULT_IMDS_ENDPOINT;
}
return new Imds(
logger,
nodeStorage,
networkClient,
cryptoProvider,
validatedIdentityEndpoint
);
}
public createRequest(
resource: string,
managedIdentityId: ManagedIdentityId
): ManagedIdentityRequestParameters {
const request: ManagedIdentityRequestParameters =
new ManagedIdentityRequestParameters(
HttpMethod.GET,
this.identityEndpoint
);
request.headers[METADATA_HEADER_NAME] = "true";
request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] =
IMDS_API_VERSION;
request.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] =
resource;
if (
managedIdentityId.idType !== ManagedIdentityIdType.SYSTEM_ASSIGNED
) {
request.queryParameters[
this.getManagedIdentityUserAssignedIdQueryParameterKey(
managedIdentityId.idType
)
] = managedIdentityId.id;
}
// bodyParameters calculated in BaseManagedIdentity.acquireTokenWithManagedIdentity
return request;
}
}
@@ -0,0 +1,147 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { INetworkModule, Logger } from "@azure/msal-common/node";
import { ManagedIdentityId } from "../../config/ManagedIdentityId.js";
import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js";
import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js";
import { NodeStorage } from "../../cache/NodeStorage.js";
import { CryptoProvider } from "../../crypto/CryptoProvider.js";
import {
API_VERSION_QUERY_PARAMETER_NAME,
HttpMethod,
ManagedIdentityEnvironmentVariableNames,
ManagedIdentityIdType,
ManagedIdentitySourceNames,
RESOURCE_BODY_OR_QUERY_PARAMETER_NAME,
SERVICE_FABRIC_SECRET_HEADER_NAME,
} from "../../utils/Constants.js";
// MSI Constants. Docs for MSI are available here https://docs.microsoft.com/azure/app-service/overview-managed-identity
const SERVICE_FABRIC_MSI_API_VERSION: string = "2019-07-01-preview";
/**
* Original source of code: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/ServiceFabricManagedIdentitySource.cs
*/
export class ServiceFabric extends BaseManagedIdentitySource {
private identityEndpoint: string;
private identityHeader: string;
constructor(
logger: Logger,
nodeStorage: NodeStorage,
networkClient: INetworkModule,
cryptoProvider: CryptoProvider,
identityEndpoint: string,
identityHeader: string
) {
super(logger, nodeStorage, networkClient, cryptoProvider);
this.identityEndpoint = identityEndpoint;
this.identityHeader = identityHeader;
}
public static getEnvironmentVariables(): Array<string | undefined> {
const identityEndpoint: string | undefined =
process.env[
ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT
];
const identityHeader: string | undefined =
process.env[
ManagedIdentityEnvironmentVariableNames.IDENTITY_HEADER
];
const identityServerThumbprint: string | undefined =
process.env[
ManagedIdentityEnvironmentVariableNames
.IDENTITY_SERVER_THUMBPRINT
];
return [identityEndpoint, identityHeader, identityServerThumbprint];
}
public static tryCreate(
logger: Logger,
nodeStorage: NodeStorage,
networkClient: INetworkModule,
cryptoProvider: CryptoProvider,
managedIdentityId: ManagedIdentityId
): ServiceFabric | null {
const [identityEndpoint, identityHeader, identityServerThumbprint] =
ServiceFabric.getEnvironmentVariables();
/*
* if either of the identity endpoint, identity header, or identity server thumbprint
* environment variables are undefined, this MSI provider is unavailable.
*/
if (!identityEndpoint || !identityHeader || !identityServerThumbprint) {
logger.info(
`[Managed Identity] ${ManagedIdentitySourceNames.SERVICE_FABRIC} managed identity is unavailable because one or all of the '${ManagedIdentityEnvironmentVariableNames.IDENTITY_HEADER}', '${ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT}' or '${ManagedIdentityEnvironmentVariableNames.IDENTITY_SERVER_THUMBPRINT}' environment variables are not defined.`
);
return null;
}
const validatedIdentityEndpoint: string =
ServiceFabric.getValidatedEnvVariableUrlString(
ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT,
identityEndpoint,
ManagedIdentitySourceNames.SERVICE_FABRIC,
logger
);
logger.info(
`[Managed Identity] Environment variables validation passed for ${ManagedIdentitySourceNames.SERVICE_FABRIC} managed identity. Endpoint URI: ${validatedIdentityEndpoint}. Creating ${ManagedIdentitySourceNames.SERVICE_FABRIC} managed identity.`
);
if (
managedIdentityId.idType !== ManagedIdentityIdType.SYSTEM_ASSIGNED
) {
logger.warning(
`[Managed Identity] ${ManagedIdentitySourceNames.SERVICE_FABRIC} user assigned managed identity is configured in the cluster, not during runtime. See also: https://learn.microsoft.com/en-us/azure/service-fabric/configure-existing-cluster-enable-managed-identity-token-service.`
);
}
return new ServiceFabric(
logger,
nodeStorage,
networkClient,
cryptoProvider,
identityEndpoint,
identityHeader
);
}
public createRequest(
resource: string,
managedIdentityId: ManagedIdentityId
): ManagedIdentityRequestParameters {
const request: ManagedIdentityRequestParameters =
new ManagedIdentityRequestParameters(
HttpMethod.GET,
this.identityEndpoint
);
request.headers[SERVICE_FABRIC_SECRET_HEADER_NAME] =
this.identityHeader;
request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] =
SERVICE_FABRIC_MSI_API_VERSION;
request.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] =
resource;
if (
managedIdentityId.idType !== ManagedIdentityIdType.SYSTEM_ASSIGNED
) {
request.queryParameters[
this.getManagedIdentityUserAssignedIdQueryParameterKey(
managedIdentityId.idType
)
] = managedIdentityId.id;
}
// bodyParameters calculated in BaseManagedIdentity.acquireTokenWithManagedIdentity
return request;
}
}
+386
View File
@@ -0,0 +1,386 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AADServerParamKeys,
AccessTokenEntity,
AccountEntity,
AccountInfo,
AuthenticationResult,
AuthenticationScheme,
Authority,
AuthToken,
BaseClient,
CacheOutcome,
ClientAuthErrorCodes,
ClientConfiguration,
CommonOnBehalfOfRequest,
Constants,
createClientAuthError,
CredentialFilter,
CredentialType,
GrantType,
IdTokenEntity,
RequestParameterBuilder,
RequestThumbprint,
ResponseHandler,
ScopeSet,
TimeUtils,
TokenClaims,
UrlString,
ClientAssertion,
getClientAssertion,
} from "@azure/msal-common/node";
import { EncodingUtils } from "../utils/EncodingUtils.js";
/**
* On-Behalf-Of client
* @public
*/
export class OnBehalfOfClient extends BaseClient {
private scopeSet: ScopeSet;
private userAssertionHash: string;
constructor(configuration: ClientConfiguration) {
super(configuration);
}
/**
* Public API to acquire tokens with on behalf of flow
* @param request - developer provided CommonOnBehalfOfRequest
*/
public async acquireToken(
request: CommonOnBehalfOfRequest
): Promise<AuthenticationResult | null> {
this.scopeSet = new ScopeSet(request.scopes || []);
// generate the user_assertion_hash for OBOAssertion
this.userAssertionHash = await this.cryptoUtils.hashString(
request.oboAssertion
);
if (request.skipCache || request.claims) {
return this.executeTokenRequest(
request,
this.authority,
this.userAssertionHash
);
}
try {
return await this.getCachedAuthenticationResult(request);
} catch (e) {
// Any failure falls back to interactive request, once we implement distributed cache, we plan to handle `createRefreshRequiredError` to refresh using the RT
return await this.executeTokenRequest(
request,
this.authority,
this.userAssertionHash
);
}
}
/**
* look up cache for tokens
* Find idtoken in the cache
* Find accessToken based on user assertion and account info in the cache
* Please note we are not yet supported OBO tokens refreshed with long lived RT. User will have to send a new assertion if the current access token expires
* This is to prevent security issues when the assertion changes over time, however, longlived RT helps retaining the session
* @param request - developer provided CommonOnBehalfOfRequest
*/
private async getCachedAuthenticationResult(
request: CommonOnBehalfOfRequest
): Promise<AuthenticationResult | null> {
// look in the cache for the access_token which matches the incoming_assertion
const cachedAccessToken = this.readAccessTokenFromCacheForOBO(
this.config.authOptions.clientId,
request
);
if (!cachedAccessToken) {
// Must refresh due to non-existent access_token.
this.serverTelemetryManager?.setCacheOutcome(
CacheOutcome.NO_CACHED_ACCESS_TOKEN
);
this.logger.info(
"SilentFlowClient:acquireCachedToken - No access token found in cache for the given properties."
);
throw createClientAuthError(
ClientAuthErrorCodes.tokenRefreshRequired
);
} else if (
TimeUtils.isTokenExpired(
cachedAccessToken.expiresOn,
this.config.systemOptions.tokenRenewalOffsetSeconds
)
) {
// Access token expired, will need to renewed
this.serverTelemetryManager?.setCacheOutcome(
CacheOutcome.CACHED_ACCESS_TOKEN_EXPIRED
);
this.logger.info(
`OnbehalfofFlow:getCachedAuthenticationResult - Cached access token is expired or will expire within ${this.config.systemOptions.tokenRenewalOffsetSeconds} seconds.`
);
throw createClientAuthError(
ClientAuthErrorCodes.tokenRefreshRequired
);
}
// fetch the idToken from cache
const cachedIdToken = this.readIdTokenFromCacheForOBO(
cachedAccessToken.homeAccountId,
request.correlationId
);
let idTokenClaims: TokenClaims | undefined;
let cachedAccount: AccountEntity | null = null;
if (cachedIdToken) {
idTokenClaims = AuthToken.extractTokenClaims(
cachedIdToken.secret,
EncodingUtils.base64Decode
);
const localAccountId = idTokenClaims.oid || idTokenClaims.sub;
const accountInfo: AccountInfo = {
homeAccountId: cachedIdToken.homeAccountId,
environment: cachedIdToken.environment,
tenantId: cachedIdToken.realm,
username: Constants.EMPTY_STRING,
localAccountId: localAccountId || Constants.EMPTY_STRING,
};
cachedAccount = this.cacheManager.readAccountFromCache(
accountInfo,
request.correlationId
);
}
// increment telemetry cache hit counter
if (this.config.serverTelemetryManager) {
this.config.serverTelemetryManager.incrementCacheHits();
}
return ResponseHandler.generateAuthenticationResult(
this.cryptoUtils,
this.authority,
{
account: cachedAccount,
accessToken: cachedAccessToken,
idToken: cachedIdToken,
refreshToken: null,
appMetadata: null,
},
true,
request,
idTokenClaims
);
}
/**
* read idtoken from cache, this is a specific implementation for OBO as the requirements differ from a generic lookup in the cacheManager
* Certain use cases of OBO flow do not expect an idToken in the cache/or from the service
* @param atHomeAccountId - account id
*/
private readIdTokenFromCacheForOBO(
atHomeAccountId: string,
correlationId: string
): IdTokenEntity | null {
const idTokenFilter: CredentialFilter = {
homeAccountId: atHomeAccountId,
environment:
this.authority.canonicalAuthorityUrlComponents.HostNameAndPort,
credentialType: CredentialType.ID_TOKEN,
clientId: this.config.authOptions.clientId,
realm: this.authority.tenant,
};
const idTokenMap: Map<string, IdTokenEntity> =
this.cacheManager.getIdTokensByFilter(idTokenFilter, correlationId);
// When acquiring a token on behalf of an application, there might not be an id token in the cache
if (Object.values(idTokenMap).length < 1) {
return null;
}
return Object.values(idTokenMap)[0] as IdTokenEntity;
}
/**
* Fetches the cached access token based on incoming assertion
* @param clientId - client id
* @param request - developer provided CommonOnBehalfOfRequest
*/
private readAccessTokenFromCacheForOBO(
clientId: string,
request: CommonOnBehalfOfRequest
) {
const authScheme =
request.authenticationScheme || AuthenticationScheme.BEARER;
/*
* Distinguish between Bearer and PoP/SSH token cache types
* Cast to lowercase to handle "bearer" from ADFS
*/
const credentialType =
authScheme &&
authScheme.toLowerCase() !==
AuthenticationScheme.BEARER.toLowerCase()
? CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME
: CredentialType.ACCESS_TOKEN;
const accessTokenFilter: CredentialFilter = {
credentialType: credentialType,
clientId,
target: ScopeSet.createSearchScopes(this.scopeSet.asArray()),
tokenType: authScheme,
keyId: request.sshKid,
requestedClaimsHash: request.requestedClaimsHash,
userAssertionHash: this.userAssertionHash,
};
const accessTokens = this.cacheManager.getAccessTokensByFilter(
accessTokenFilter,
request.correlationId
);
const numAccessTokens = accessTokens.length;
if (numAccessTokens < 1) {
return null;
} else if (numAccessTokens > 1) {
throw createClientAuthError(
ClientAuthErrorCodes.multipleMatchingTokens
);
}
return accessTokens[0] as AccessTokenEntity;
}
/**
* Make a network call to the server requesting credentials
* @param request - developer provided CommonOnBehalfOfRequest
* @param authority - authority object
*/
private async executeTokenRequest(
request: CommonOnBehalfOfRequest,
authority: Authority,
userAssertionHash: string
): Promise<AuthenticationResult | null> {
const queryParametersString = this.createTokenQueryParameters(request);
const endpoint = UrlString.appendQueryString(
authority.tokenEndpoint,
queryParametersString
);
const requestBody = await this.createTokenRequestBody(request);
const headers: Record<string, string> =
this.createTokenRequestHeaders();
const thumbprint: RequestThumbprint = {
clientId: this.config.authOptions.clientId,
authority: request.authority,
scopes: request.scopes,
claims: request.claims,
authenticationScheme: request.authenticationScheme,
resourceRequestMethod: request.resourceRequestMethod,
resourceRequestUri: request.resourceRequestUri,
shrClaims: request.shrClaims,
sshKid: request.sshKid,
};
const reqTimestamp = TimeUtils.nowSeconds();
const response = await this.executePostToTokenEndpoint(
endpoint,
requestBody,
headers,
thumbprint,
request.correlationId
);
const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
this.cacheManager,
this.cryptoUtils,
this.logger,
this.config.serializableCache,
this.config.persistencePlugin
);
responseHandler.validateTokenResponse(response.body);
const tokenResponse = await responseHandler.handleServerTokenResponse(
response.body,
this.authority,
reqTimestamp,
request,
undefined,
userAssertionHash
);
return tokenResponse;
}
/**
* generate a server request in accepable format
* @param request - developer provided CommonOnBehalfOfRequest
*/
private async createTokenRequestBody(
request: CommonOnBehalfOfRequest
): Promise<string> {
const parameterBuilder = new RequestParameterBuilder();
parameterBuilder.addClientId(this.config.authOptions.clientId);
parameterBuilder.addScopes(request.scopes);
parameterBuilder.addGrantType(GrantType.JWT_BEARER);
parameterBuilder.addClientInfo();
parameterBuilder.addLibraryInfo(this.config.libraryInfo);
parameterBuilder.addApplicationTelemetry(
this.config.telemetry.application
);
parameterBuilder.addThrottling();
if (this.serverTelemetryManager) {
parameterBuilder.addServerTelemetry(this.serverTelemetryManager);
}
const correlationId =
request.correlationId ||
this.config.cryptoInterface.createNewGuid();
parameterBuilder.addCorrelationId(correlationId);
parameterBuilder.addRequestTokenUse(AADServerParamKeys.ON_BEHALF_OF);
parameterBuilder.addOboAssertion(request.oboAssertion);
if (this.config.clientCredentials.clientSecret) {
parameterBuilder.addClientSecret(
this.config.clientCredentials.clientSecret
);
}
const clientAssertion: ClientAssertion | undefined =
this.config.clientCredentials.clientAssertion;
if (clientAssertion) {
parameterBuilder.addClientAssertion(
await getClientAssertion(
clientAssertion.assertion,
this.config.authOptions.clientId,
request.resourceRequestUri
)
);
parameterBuilder.addClientAssertionType(
clientAssertion.assertionType
);
}
if (
request.claims ||
(this.config.authOptions.clientCapabilities &&
this.config.authOptions.clientCapabilities.length > 0)
) {
parameterBuilder.addClaims(
request.claims,
this.config.authOptions.clientCapabilities
);
}
return parameterBuilder.createQueryString();
}
}
+355
View File
@@ -0,0 +1,355 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ApiId,
Constants,
LOOPBACK_SERVER_CONSTANTS,
} from "../utils/Constants.js";
import {
AuthenticationResult,
CommonDeviceCodeRequest,
AuthError,
ResponseMode,
OIDC_DEFAULT_SCOPES,
CodeChallengeMethodValues,
Constants as CommonConstants,
ServerError,
NativeRequest,
NativeSignOutRequest,
AccountInfo,
INativeBrokerPlugin,
ServerAuthorizationCodeResponse,
AADServerParamKeys,
ServerTelemetryManager,
} from "@azure/msal-common/node";
import { Configuration } from "../config/Configuration.js";
import { ClientApplication } from "./ClientApplication.js";
import { IPublicClientApplication } from "./IPublicClientApplication.js";
import { DeviceCodeRequest } from "../request/DeviceCodeRequest.js";
import { AuthorizationUrlRequest } from "../request/AuthorizationUrlRequest.js";
import { AuthorizationCodeRequest } from "../request/AuthorizationCodeRequest.js";
import { InteractiveRequest } from "../request/InteractiveRequest.js";
import { NodeAuthError, NodeAuthErrorMessage } from "../error/NodeAuthError.js";
import { LoopbackClient } from "../network/LoopbackClient.js";
import { SilentFlowRequest } from "../request/SilentFlowRequest.js";
import { SignOutRequest } from "../request/SignOutRequest.js";
import { ILoopbackClient } from "../network/ILoopbackClient.js";
import { DeviceCodeClient } from "./DeviceCodeClient.js";
import { version } from "../packageMetadata.js";
/**
* This class is to be used to acquire tokens for public client applications (desktop, mobile). Public client applications
* are not trusted to safely store application secrets, and therefore can only request tokens in the name of an user.
* @public
*/
export class PublicClientApplication
extends ClientApplication
implements IPublicClientApplication
{
private nativeBrokerPlugin?: INativeBrokerPlugin;
private readonly skus: string;
/**
* Important attributes in the Configuration object for auth are:
* - clientID: the application ID of your application. You can obtain one by registering your application with our Application registration portal.
* - authority: the authority URL for your application.
*
* AAD authorities are of the form https://login.microsoftonline.com/\{Enter_the_Tenant_Info_Here\}.
* - If your application supports Accounts in one organizational directory, replace "Enter_the_Tenant_Info_Here" value with the Tenant Id or Tenant name (for example, contoso.microsoft.com).
* - If your application supports Accounts in any organizational directory, replace "Enter_the_Tenant_Info_Here" value with organizations.
* - If your application supports Accounts in any organizational directory and personal Microsoft accounts, replace "Enter_the_Tenant_Info_Here" value with common.
* - To restrict support to Personal Microsoft accounts only, replace "Enter_the_Tenant_Info_Here" value with consumers.
*
* Azure B2C authorities are of the form https://\{instance\}/\{tenant\}/\{policy\}. Each policy is considered
* its own authority. You will have to set the all of the knownAuthorities at the time of the client application
* construction.
*
* ADFS authorities are of the form https://\{instance\}/adfs.
*/
constructor(configuration: Configuration) {
super(configuration);
if (this.config.broker.nativeBrokerPlugin) {
if (this.config.broker.nativeBrokerPlugin.isBrokerAvailable) {
this.nativeBrokerPlugin = this.config.broker.nativeBrokerPlugin;
this.nativeBrokerPlugin.setLogger(
this.config.system.loggerOptions
);
} else {
this.logger.warning(
"NativeBroker implementation was provided but the broker is unavailable."
);
}
}
this.skus = ServerTelemetryManager.makeExtraSkuString({
libraryName: Constants.MSAL_SKU,
libraryVersion: version,
});
}
/**
* Acquires a token from the authority using OAuth2.0 device code flow.
* This flow is designed for devices that do not have access to a browser or have input constraints.
* The authorization server issues a DeviceCode object with a verification code, an end-user code,
* and the end-user verification URI. The DeviceCode object is provided through a callback, and the end-user should be
* instructed to use another device to navigate to the verification URI to input credentials.
* Since the client cannot receive incoming requests, it polls the authorization server repeatedly
* until the end-user completes input of credentials.
*/
public async acquireTokenByDeviceCode(
request: DeviceCodeRequest
): Promise<AuthenticationResult | null> {
this.logger.info(
"acquireTokenByDeviceCode called",
request.correlationId
);
const validRequest: CommonDeviceCodeRequest = Object.assign(
request,
await this.initializeBaseRequest(request)
);
const serverTelemetryManager = this.initializeServerTelemetryManager(
ApiId.acquireTokenByDeviceCode,
validRequest.correlationId
);
try {
const deviceCodeConfig = await this.buildOauthClientConfiguration(
validRequest.authority,
validRequest.correlationId,
"",
serverTelemetryManager,
undefined,
request.azureCloudOptions
);
const deviceCodeClient = new DeviceCodeClient(deviceCodeConfig);
this.logger.verbose(
"Device code client created",
validRequest.correlationId
);
return await deviceCodeClient.acquireToken(validRequest);
} catch (e) {
if (e instanceof AuthError) {
e.setCorrelationId(validRequest.correlationId);
}
serverTelemetryManager.cacheFailedRequest(e as AuthError);
throw e;
}
}
/**
* Acquires a token interactively via the browser by requesting an authorization code then exchanging it for a token.
*/
async acquireTokenInteractive(
request: InteractiveRequest
): Promise<AuthenticationResult> {
const correlationId =
request.correlationId || this.cryptoProvider.createNewGuid();
this.logger.trace("acquireTokenInteractive called", correlationId);
const {
openBrowser,
successTemplate,
errorTemplate,
windowHandle,
loopbackClient: customLoopbackClient,
...remainingProperties
} = request;
if (this.nativeBrokerPlugin) {
const brokerRequest: NativeRequest = {
...remainingProperties,
clientId: this.config.auth.clientId,
scopes: request.scopes || OIDC_DEFAULT_SCOPES,
redirectUri: `${Constants.HTTP_PROTOCOL}${Constants.LOCALHOST}`,
authority: request.authority || this.config.auth.authority,
correlationId: correlationId,
extraParameters: {
...remainingProperties.extraQueryParameters,
...remainingProperties.tokenQueryParameters,
[AADServerParamKeys.X_CLIENT_EXTRA_SKU]: this.skus,
},
accountId: remainingProperties.account?.nativeAccountId,
};
return this.nativeBrokerPlugin.acquireTokenInteractive(
brokerRequest,
windowHandle
);
}
const { verifier, challenge } =
await this.cryptoProvider.generatePkceCodes();
const loopbackClient: ILoopbackClient =
customLoopbackClient || new LoopbackClient();
let authCodeResponse: ServerAuthorizationCodeResponse = {};
let authCodeListenerError: AuthError | null = null;
try {
const authCodeListener = loopbackClient
.listenForAuthCode(successTemplate, errorTemplate)
.then((response) => {
authCodeResponse = response;
})
.catch((e) => {
// Store the promise instead of throwing so we can control when its thrown
authCodeListenerError = e;
});
// Wait for server to be listening
const redirectUri = await this.waitForRedirectUri(loopbackClient);
const validRequest: AuthorizationUrlRequest = {
...remainingProperties,
correlationId: correlationId,
scopes: request.scopes || OIDC_DEFAULT_SCOPES,
redirectUri: redirectUri,
responseMode: ResponseMode.QUERY,
codeChallenge: challenge,
codeChallengeMethod: CodeChallengeMethodValues.S256,
};
const authCodeUrl = await this.getAuthCodeUrl(validRequest);
await openBrowser(authCodeUrl);
await authCodeListener;
if (authCodeListenerError) {
throw authCodeListenerError;
}
if (authCodeResponse.error) {
throw new ServerError(
authCodeResponse.error,
authCodeResponse.error_description,
authCodeResponse.suberror
);
} else if (!authCodeResponse.code) {
throw NodeAuthError.createNoAuthCodeInResponseError();
}
const clientInfo = authCodeResponse.client_info;
const tokenRequest: AuthorizationCodeRequest = {
code: authCodeResponse.code,
codeVerifier: verifier,
clientInfo: clientInfo || CommonConstants.EMPTY_STRING,
...validRequest,
};
return await this.acquireTokenByCode(tokenRequest); // Await this so the server doesn't close prematurely
} finally {
loopbackClient.closeServer();
}
}
/**
* Returns a token retrieved either from the cache or by exchanging the refresh token for a fresh access token. If brokering is enabled the token request will be serviced by the broker.
* @param request - developer provided SilentFlowRequest
* @returns
*/
async acquireTokenSilent(
request: SilentFlowRequest
): Promise<AuthenticationResult> {
const correlationId =
request.correlationId || this.cryptoProvider.createNewGuid();
this.logger.trace("acquireTokenSilent called", correlationId);
if (this.nativeBrokerPlugin) {
const brokerRequest: NativeRequest = {
...request,
clientId: this.config.auth.clientId,
scopes: request.scopes || OIDC_DEFAULT_SCOPES,
redirectUri: `${Constants.HTTP_PROTOCOL}${Constants.LOCALHOST}`,
authority: request.authority || this.config.auth.authority,
correlationId: correlationId,
extraParameters: {
...request.tokenQueryParameters,
[AADServerParamKeys.X_CLIENT_EXTRA_SKU]: this.skus,
},
accountId: request.account.nativeAccountId,
forceRefresh: request.forceRefresh || false,
};
return this.nativeBrokerPlugin.acquireTokenSilent(brokerRequest);
}
return super.acquireTokenSilent(request);
}
/**
* Removes cache artifacts associated with the given account
* @param request - developer provided SignOutRequest
* @returns
*/
async signOut(request: SignOutRequest): Promise<void> {
if (this.nativeBrokerPlugin && request.account.nativeAccountId) {
const signoutRequest: NativeSignOutRequest = {
clientId: this.config.auth.clientId,
accountId: request.account.nativeAccountId,
correlationId:
request.correlationId ||
this.cryptoProvider.createNewGuid(),
};
await this.nativeBrokerPlugin.signOut(signoutRequest);
}
await this.getTokenCache().removeAccount(
request.account,
request.correlationId
);
}
/**
* Returns all cached accounts for this application. If brokering is enabled this request will be serviced by the broker.
* @returns
*/
async getAllAccounts(): Promise<AccountInfo[]> {
if (this.nativeBrokerPlugin) {
const correlationId = this.cryptoProvider.createNewGuid();
return this.nativeBrokerPlugin.getAllAccounts(
this.config.auth.clientId,
correlationId
);
}
return this.getTokenCache().getAllAccounts();
}
/**
* Attempts to retrieve the redirectUri from the loopback server. If the loopback server does not start listening for requests within the timeout this will throw.
* @param loopbackClient - developer provided custom loopback server implementation
* @returns
*/
private async waitForRedirectUri(
loopbackClient: ILoopbackClient
): Promise<string> {
return new Promise<string>((resolve, reject) => {
let ticks = 0;
const id = setInterval(() => {
if (
LOOPBACK_SERVER_CONSTANTS.TIMEOUT_MS /
LOOPBACK_SERVER_CONSTANTS.INTERVAL_MS <
ticks
) {
clearInterval(id);
reject(NodeAuthError.createLoopbackServerTimeoutError());
return;
}
try {
const r = loopbackClient.getRedirectUri();
clearInterval(id);
resolve(r);
return;
} catch (e) {
if (
e instanceof AuthError &&
e.errorCode ===
NodeAuthErrorMessage.noLoopbackServerExists.code
) {
// Loopback server is not listening yet
ticks++;
return;
}
clearInterval(id);
reject(e);
return;
}
}, LOOPBACK_SERVER_CONSTANTS.INTERVAL_MS);
});
}
}
+190
View File
@@ -0,0 +1,190 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AuthenticationResult,
Authority,
BaseClient,
CcsCredentialType,
ClientAssertion,
ClientConfiguration,
CommonUsernamePasswordRequest,
GrantType,
NetworkResponse,
RequestParameterBuilder,
RequestThumbprint,
ResponseHandler,
ServerAuthorizationTokenResponse,
StringUtils,
TimeUtils,
UrlString,
getClientAssertion,
} from "@azure/msal-common/node";
/**
* Oauth2.0 Password grant client
* Note: We are only supporting public clients for password grant and for purely testing purposes
* @public
*/
export class UsernamePasswordClient extends BaseClient {
constructor(configuration: ClientConfiguration) {
super(configuration);
}
/**
* API to acquire a token by passing the username and password to the service in exchage of credentials
* password_grant
* @param request - CommonUsernamePasswordRequest
*/
async acquireToken(
request: CommonUsernamePasswordRequest
): Promise<AuthenticationResult | null> {
this.logger.info("in acquireToken call in username-password client");
const reqTimestamp = TimeUtils.nowSeconds();
const response = await this.executeTokenRequest(
this.authority,
request
);
const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
this.cacheManager,
this.cryptoUtils,
this.logger,
this.config.serializableCache,
this.config.persistencePlugin
);
// Validate response. This function throws a server error if an error is returned by the server.
responseHandler.validateTokenResponse(response.body);
const tokenResponse = responseHandler.handleServerTokenResponse(
response.body,
this.authority,
reqTimestamp,
request
);
return tokenResponse;
}
/**
* Executes POST request to token endpoint
* @param authority - authority object
* @param request - CommonUsernamePasswordRequest provided by the developer
*/
private async executeTokenRequest(
authority: Authority,
request: CommonUsernamePasswordRequest
): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {
const queryParametersString = this.createTokenQueryParameters(request);
const endpoint = UrlString.appendQueryString(
authority.tokenEndpoint,
queryParametersString
);
const requestBody = await this.createTokenRequestBody(request);
const headers: Record<string, string> = this.createTokenRequestHeaders({
credential: request.username,
type: CcsCredentialType.UPN,
});
const thumbprint: RequestThumbprint = {
clientId: this.config.authOptions.clientId,
authority: authority.canonicalAuthority,
scopes: request.scopes,
claims: request.claims,
authenticationScheme: request.authenticationScheme,
resourceRequestMethod: request.resourceRequestMethod,
resourceRequestUri: request.resourceRequestUri,
shrClaims: request.shrClaims,
sshKid: request.sshKid,
};
return this.executePostToTokenEndpoint(
endpoint,
requestBody,
headers,
thumbprint,
request.correlationId
);
}
/**
* Generates a map for all the params to be sent to the service
* @param request - CommonUsernamePasswordRequest provided by the developer
*/
private async createTokenRequestBody(
request: CommonUsernamePasswordRequest
): Promise<string> {
const parameterBuilder = new RequestParameterBuilder();
parameterBuilder.addClientId(this.config.authOptions.clientId);
parameterBuilder.addUsername(request.username);
parameterBuilder.addPassword(request.password);
parameterBuilder.addScopes(request.scopes);
parameterBuilder.addResponseTypeForTokenAndIdToken();
parameterBuilder.addGrantType(GrantType.RESOURCE_OWNER_PASSWORD_GRANT);
parameterBuilder.addClientInfo();
parameterBuilder.addLibraryInfo(this.config.libraryInfo);
parameterBuilder.addApplicationTelemetry(
this.config.telemetry.application
);
parameterBuilder.addThrottling();
if (this.serverTelemetryManager) {
parameterBuilder.addServerTelemetry(this.serverTelemetryManager);
}
const correlationId =
request.correlationId ||
this.config.cryptoInterface.createNewGuid();
parameterBuilder.addCorrelationId(correlationId);
if (this.config.clientCredentials.clientSecret) {
parameterBuilder.addClientSecret(
this.config.clientCredentials.clientSecret
);
}
const clientAssertion: ClientAssertion | undefined =
this.config.clientCredentials.clientAssertion;
if (clientAssertion) {
parameterBuilder.addClientAssertion(
await getClientAssertion(
clientAssertion.assertion,
this.config.authOptions.clientId,
request.resourceRequestUri
)
);
parameterBuilder.addClientAssertionType(
clientAssertion.assertionType
);
}
if (
!StringUtils.isEmptyObj(request.claims) ||
(this.config.authOptions.clientCapabilities &&
this.config.authOptions.clientCapabilities.length > 0)
) {
parameterBuilder.addClaims(
request.claims,
this.config.authOptions.clientCapabilities
);
}
if (
this.config.systemOptions.preventCorsPreflight &&
request.username
) {
parameterBuilder.addCcsUpn(request.username);
}
return parameterBuilder.createQueryString();
}
}