ruchy-syntax-tools
Version:
Comprehensive syntax highlighting and language support for the Ruchy programming language
684 lines (642 loc) • 25.7 kB
text/typescript
/*
* 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,
CacheOutcome,
ClientAuthError,
} 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";
import { getAuthCodeRequestUrl } from "../protocol/Authorize.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,
state: request.state || "",
nonce: request.nonce || "",
};
const discoveredAuthority = await this.createAuthority(
validRequest.authority,
validRequest.correlationId,
undefined,
request.azureCloudOptions
);
return getAuthCodeRequestUrl(
this.config,
discoveredAuthority,
validRequest,
this.logger
);
}
/**
* 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 discoveredAuthority = await this.createAuthority(
validRequest.authority,
validRequest.correlationId,
undefined,
request.azureCloudOptions
);
const authClientConfig = await this.buildOauthClientConfiguration(
discoveredAuthority,
validRequest.correlationId,
validRequest.redirectUri,
serverTelemetryManager
);
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 discoveredAuthority = await this.createAuthority(
validRequest.authority,
validRequest.correlationId,
undefined,
request.azureCloudOptions
);
const refreshTokenClientConfig =
await this.buildOauthClientConfiguration(
discoveredAuthority,
validRequest.correlationId,
validRequest.redirectUri || "",
serverTelemetryManager
);
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 discoveredAuthority = await this.createAuthority(
validRequest.authority,
validRequest.correlationId,
undefined,
request.azureCloudOptions
);
const clientConfiguration =
await this.buildOauthClientConfiguration(
discoveredAuthority,
validRequest.correlationId,
validRequest.redirectUri || "",
serverTelemetryManager
);
const silentFlowClient = new SilentFlowClient(clientConfiguration);
this.logger.verbose(
"Silent flow client created",
validRequest.correlationId
);
try {
// always overwrite the in-memory cache with the persistence cache (if it exists) before a cache lookup
await this.tokenCache.overwriteCache();
return await this.acquireCachedTokenSilent(
validRequest,
silentFlowClient,
clientConfiguration
);
} catch (error) {
if (
error instanceof ClientAuthError &&
error.errorCode ===
ClientAuthErrorCodes.tokenRefreshRequired
) {
const refreshTokenClient = new RefreshTokenClient(
clientConfiguration
);
return refreshTokenClient.acquireTokenByRefreshToken(
validRequest
);
}
throw error;
}
} catch (error) {
if (error instanceof AuthError) {
error.setCorrelationId(validRequest.correlationId);
}
serverTelemetryManager.cacheFailedRequest(error);
throw error;
}
}
private async acquireCachedTokenSilent(
validRequest: CommonSilentFlowRequest,
silentFlowClient: SilentFlowClient,
clientConfiguration: ClientConfiguration
): Promise<AuthenticationResult> {
const [authResponse, cacheOutcome] =
await silentFlowClient.acquireCachedToken({
...validRequest,
scopes: validRequest.scopes?.length
? validRequest.scopes
: [...OIDC_DEFAULT_SCOPES],
});
if (cacheOutcome === CacheOutcome.PROACTIVELY_REFRESHED) {
this.logger.info(
"ClientApplication:acquireCachedTokenSilent - 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 refreshTokenClient = new RefreshTokenClient(
clientConfiguration
);
try {
await refreshTokenClient.acquireTokenByRefreshToken(
validRequest
);
} catch {
// do nothing, this is running in the background and no action is to be taken upon success or failure
}
}
// return the cached token
return authResponse;
}
/**
* 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
* @deprecated - Use a more secure flow instead
*/
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 discoveredAuthority = await this.createAuthority(
validRequest.authority,
validRequest.correlationId,
undefined,
request.azureCloudOptions
);
const usernamePasswordClientConfig =
await this.buildOauthClientConfiguration(
discoveredAuthority,
validRequest.correlationId,
"",
serverTelemetryManager
);
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(
discoveredAuthority: Authority,
requestCorrelationId: string,
redirectUri: string,
serverTelemetryManager?: ServerTelemetryManager
): Promise<ClientConfiguration> {
this.logger.verbose(
"buildOauthClientConfiguration called",
requestCorrelationId
);
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
*/
protected 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 || this.config.auth.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();
}
}