Add PBandGraphOauth scaffold and auth UI
This commit is contained in:
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
import { AccountInfo } from "../account/AccountInfo.js";
|
||||
|
||||
/**
|
||||
* Result returned from the authority's token endpoint.
|
||||
* - uniqueId - `oid` or `sub` claim from ID token
|
||||
* - tenantId - `tid` claim from ID token
|
||||
* - scopes - Scopes that are validated for the respective token
|
||||
* - account - An account object representation of the currently signed-in user
|
||||
* - idToken - Id token received as part of the response
|
||||
* - idTokenClaims - MSAL-relevant ID token claims
|
||||
* - accessToken - Access token or SSH certificate received as part of the response
|
||||
* - fromCache - Boolean denoting whether token came from cache
|
||||
* - expiresOn - Javascript Date object representing relative expiration of access token
|
||||
* - extExpiresOn - Javascript Date object representing extended relative expiration of access token in case of server outage
|
||||
* - refreshOn - Javascript Date object representing relative time until an access token must be refreshed
|
||||
* - state - Value passed in by user in request
|
||||
* - familyId - Family ID identifier, usually only used for refresh tokens
|
||||
* - requestId - Request ID returned as part of the response
|
||||
*/
|
||||
export type AuthenticationResult = {
|
||||
authority: string;
|
||||
uniqueId: string;
|
||||
tenantId: string;
|
||||
scopes: Array<string>;
|
||||
account: AccountInfo | null;
|
||||
idToken: string;
|
||||
idTokenClaims: object;
|
||||
accessToken: string;
|
||||
fromCache: boolean;
|
||||
expiresOn: Date | null;
|
||||
extExpiresOn?: Date;
|
||||
refreshOn?: Date;
|
||||
tokenType: string;
|
||||
correlationId: string;
|
||||
requestId?: string;
|
||||
state?: string;
|
||||
familyId?: string;
|
||||
cloudGraphHostName?: string;
|
||||
msGraphHost?: string;
|
||||
code?: string;
|
||||
fromNativeBroker?: boolean;
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Response returned after processing the code response query string or fragment.
|
||||
*/
|
||||
export type AuthorizationCodePayload = {
|
||||
code: string;
|
||||
cloud_instance_name?: string;
|
||||
cloud_instance_host_name?: string;
|
||||
cloud_graph_host_name?: string;
|
||||
msgraph_host?: string;
|
||||
state?: string;
|
||||
nonce?: string;
|
||||
client_info?: string;
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* DeviceCode returned by the security token service device code endpoint containing information necessary for device code flow.
|
||||
* - userCode: code which user needs to provide when authenticating at the verification URI
|
||||
* - deviceCode: code which should be included in the request for the access token
|
||||
* - verificationUri: URI where user can authenticate
|
||||
* - expiresIn: expiration time of the device code in seconds
|
||||
* - interval: interval at which the STS should be polled at
|
||||
* - message: message which should be displayed to the user
|
||||
*/
|
||||
export type DeviceCodeResponse = {
|
||||
userCode: string;
|
||||
deviceCode: string;
|
||||
verificationUri: string;
|
||||
expiresIn: number;
|
||||
interval: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type ServerDeviceCodeResponse = {
|
||||
user_code: string;
|
||||
device_code: string;
|
||||
verification_uri: string;
|
||||
expires_in: number;
|
||||
interval: number;
|
||||
message: string;
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
import { ServerAuthorizationTokenResponse } from "./ServerAuthorizationTokenResponse.js";
|
||||
|
||||
/**
|
||||
* Response object used for loading external tokens to cache.
|
||||
* - token_type: Indicates the token type value. The only type that Azure AD supports is Bearer.
|
||||
* - scope: The scopes that the access_token is valid for.
|
||||
* - expires_in: How long the access token is valid (in seconds).
|
||||
* - id_token: A JSON Web Token (JWT). The app can decode the segments of this token to request information about the user who signed in.
|
||||
* - refresh_token: An OAuth 2.0 refresh token. The app can use this token acquire additional access tokens after the current access token expires.
|
||||
* - access_token: The requested access token. The app can use this token to authenticate to the secured resource, such as a web API.
|
||||
* - client_info: Client info object
|
||||
*/
|
||||
export type ExternalTokenResponse = Pick<
|
||||
ServerAuthorizationTokenResponse,
|
||||
| "token_type"
|
||||
| "scope"
|
||||
| "expires_in"
|
||||
| "ext_expires_in"
|
||||
| "id_token"
|
||||
| "refresh_token"
|
||||
| "refresh_token_expires_in"
|
||||
| "foci"
|
||||
> & {
|
||||
access_token?: string;
|
||||
client_info?: string;
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
export type IMDSBadResponse = {
|
||||
error: string;
|
||||
"newest-versions": Array<string>;
|
||||
};
|
||||
+775
@@ -0,0 +1,775 @@
|
||||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
import { ServerAuthorizationTokenResponse } from "./ServerAuthorizationTokenResponse.js";
|
||||
import { ICrypto } from "../crypto/ICrypto.js";
|
||||
import {
|
||||
ClientAuthErrorCodes,
|
||||
createClientAuthError,
|
||||
} from "../error/ClientAuthError.js";
|
||||
import { ServerAuthorizationCodeResponse } from "./ServerAuthorizationCodeResponse.js";
|
||||
import { Logger } from "../logger/Logger.js";
|
||||
import { ServerError } from "../error/ServerError.js";
|
||||
import { ScopeSet } from "../request/ScopeSet.js";
|
||||
import { AuthenticationResult } from "./AuthenticationResult.js";
|
||||
import { AccountEntity } from "../cache/entities/AccountEntity.js";
|
||||
import { Authority } from "../authority/Authority.js";
|
||||
import { IdTokenEntity } from "../cache/entities/IdTokenEntity.js";
|
||||
import { AccessTokenEntity } from "../cache/entities/AccessTokenEntity.js";
|
||||
import { RefreshTokenEntity } from "../cache/entities/RefreshTokenEntity.js";
|
||||
import {
|
||||
InteractionRequiredAuthError,
|
||||
isInteractionRequiredError,
|
||||
} from "../error/InteractionRequiredAuthError.js";
|
||||
import { CacheRecord } from "../cache/entities/CacheRecord.js";
|
||||
import { CacheManager } from "../cache/CacheManager.js";
|
||||
import { ProtocolUtils, RequestStateObject } from "../utils/ProtocolUtils.js";
|
||||
import {
|
||||
AuthenticationScheme,
|
||||
Constants,
|
||||
THE_FAMILY_ID,
|
||||
HttpStatus,
|
||||
} from "../utils/Constants.js";
|
||||
import { PopTokenGenerator } from "../crypto/PopTokenGenerator.js";
|
||||
import { AppMetadataEntity } from "../cache/entities/AppMetadataEntity.js";
|
||||
import { ICachePlugin } from "../cache/interface/ICachePlugin.js";
|
||||
import { TokenCacheContext } from "../cache/persistence/TokenCacheContext.js";
|
||||
import { ISerializableTokenCache } from "../cache/interface/ISerializableTokenCache.js";
|
||||
import { AuthorizationCodePayload } from "./AuthorizationCodePayload.js";
|
||||
import { BaseAuthRequest } from "../request/BaseAuthRequest.js";
|
||||
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
|
||||
import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent.js";
|
||||
import { checkMaxAge, extractTokenClaims } from "../account/AuthToken.js";
|
||||
import {
|
||||
TokenClaims,
|
||||
getTenantIdFromIdTokenClaims,
|
||||
} from "../account/TokenClaims.js";
|
||||
import {
|
||||
AccountInfo,
|
||||
buildTenantProfile,
|
||||
updateAccountTenantProfileData,
|
||||
} from "../account/AccountInfo.js";
|
||||
import * as CacheHelpers from "../cache/utils/CacheHelpers.js";
|
||||
|
||||
function parseServerErrorNo(
|
||||
serverResponse: ServerAuthorizationCodeResponse
|
||||
): string | undefined {
|
||||
const errorCodePrefix = "code=";
|
||||
const errorCodePrefixIndex =
|
||||
serverResponse.error_uri?.lastIndexOf(errorCodePrefix);
|
||||
return errorCodePrefixIndex && errorCodePrefixIndex >= 0
|
||||
? serverResponse.error_uri?.substring(
|
||||
errorCodePrefixIndex + errorCodePrefix.length
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class that handles response parsing.
|
||||
* @internal
|
||||
*/
|
||||
export class ResponseHandler {
|
||||
private clientId: string;
|
||||
private cacheStorage: CacheManager;
|
||||
private cryptoObj: ICrypto;
|
||||
private logger: Logger;
|
||||
private homeAccountIdentifier: string;
|
||||
private serializableCache: ISerializableTokenCache | null;
|
||||
private persistencePlugin: ICachePlugin | null;
|
||||
private performanceClient?: IPerformanceClient;
|
||||
|
||||
constructor(
|
||||
clientId: string,
|
||||
cacheStorage: CacheManager,
|
||||
cryptoObj: ICrypto,
|
||||
logger: Logger,
|
||||
serializableCache: ISerializableTokenCache | null,
|
||||
persistencePlugin: ICachePlugin | null,
|
||||
performanceClient?: IPerformanceClient
|
||||
) {
|
||||
this.clientId = clientId;
|
||||
this.cacheStorage = cacheStorage;
|
||||
this.cryptoObj = cryptoObj;
|
||||
this.logger = logger;
|
||||
this.serializableCache = serializableCache;
|
||||
this.persistencePlugin = persistencePlugin;
|
||||
this.performanceClient = performanceClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function which validates server authorization code response.
|
||||
* @param serverResponseHash
|
||||
* @param requestState
|
||||
* @param cryptoObj
|
||||
*/
|
||||
validateServerAuthorizationCodeResponse(
|
||||
serverResponse: ServerAuthorizationCodeResponse,
|
||||
requestState: string
|
||||
): void {
|
||||
if (!serverResponse.state || !requestState) {
|
||||
throw serverResponse.state
|
||||
? createClientAuthError(
|
||||
ClientAuthErrorCodes.stateNotFound,
|
||||
"Cached State"
|
||||
)
|
||||
: createClientAuthError(
|
||||
ClientAuthErrorCodes.stateNotFound,
|
||||
"Server State"
|
||||
);
|
||||
}
|
||||
|
||||
let decodedServerResponseState: string;
|
||||
let decodedRequestState: string;
|
||||
|
||||
try {
|
||||
decodedServerResponseState = decodeURIComponent(
|
||||
serverResponse.state
|
||||
);
|
||||
} catch (e) {
|
||||
throw createClientAuthError(
|
||||
ClientAuthErrorCodes.invalidState,
|
||||
serverResponse.state
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
decodedRequestState = decodeURIComponent(requestState);
|
||||
} catch (e) {
|
||||
throw createClientAuthError(
|
||||
ClientAuthErrorCodes.invalidState,
|
||||
serverResponse.state
|
||||
);
|
||||
}
|
||||
|
||||
if (decodedServerResponseState !== decodedRequestState) {
|
||||
throw createClientAuthError(ClientAuthErrorCodes.stateMismatch);
|
||||
}
|
||||
|
||||
// Check for error
|
||||
if (
|
||||
serverResponse.error ||
|
||||
serverResponse.error_description ||
|
||||
serverResponse.suberror
|
||||
) {
|
||||
const serverErrorNo = parseServerErrorNo(serverResponse);
|
||||
if (
|
||||
isInteractionRequiredError(
|
||||
serverResponse.error,
|
||||
serverResponse.error_description,
|
||||
serverResponse.suberror
|
||||
)
|
||||
) {
|
||||
throw new InteractionRequiredAuthError(
|
||||
serverResponse.error || "",
|
||||
serverResponse.error_description,
|
||||
serverResponse.suberror,
|
||||
serverResponse.timestamp || "",
|
||||
serverResponse.trace_id || "",
|
||||
serverResponse.correlation_id || "",
|
||||
serverResponse.claims || "",
|
||||
serverErrorNo
|
||||
);
|
||||
}
|
||||
|
||||
throw new ServerError(
|
||||
serverResponse.error || "",
|
||||
serverResponse.error_description,
|
||||
serverResponse.suberror,
|
||||
serverErrorNo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function which validates server authorization token response.
|
||||
* @param serverResponse
|
||||
* @param refreshAccessToken
|
||||
*/
|
||||
validateTokenResponse(
|
||||
serverResponse: ServerAuthorizationTokenResponse,
|
||||
refreshAccessToken?: boolean
|
||||
): void {
|
||||
// Check for error
|
||||
if (
|
||||
serverResponse.error ||
|
||||
serverResponse.error_description ||
|
||||
serverResponse.suberror
|
||||
) {
|
||||
const errString = `Error(s): ${
|
||||
serverResponse.error_codes || Constants.NOT_AVAILABLE
|
||||
} - Timestamp: ${
|
||||
serverResponse.timestamp || Constants.NOT_AVAILABLE
|
||||
} - Description: ${
|
||||
serverResponse.error_description || Constants.NOT_AVAILABLE
|
||||
} - Correlation ID: ${
|
||||
serverResponse.correlation_id || Constants.NOT_AVAILABLE
|
||||
} - Trace ID: ${
|
||||
serverResponse.trace_id || Constants.NOT_AVAILABLE
|
||||
}`;
|
||||
const serverErrorNo = serverResponse.error_codes?.length
|
||||
? serverResponse.error_codes[0]
|
||||
: undefined;
|
||||
const serverError = new ServerError(
|
||||
serverResponse.error,
|
||||
errString,
|
||||
serverResponse.suberror,
|
||||
serverErrorNo,
|
||||
serverResponse.status
|
||||
);
|
||||
|
||||
// check if 500 error
|
||||
if (
|
||||
refreshAccessToken &&
|
||||
serverResponse.status &&
|
||||
serverResponse.status >= HttpStatus.SERVER_ERROR_RANGE_START &&
|
||||
serverResponse.status <= HttpStatus.SERVER_ERROR_RANGE_END
|
||||
) {
|
||||
this.logger.warning(
|
||||
`executeTokenRequest:validateTokenResponse - AAD is currently unavailable and the access token is unable to be refreshed.\n${serverError}`
|
||||
);
|
||||
|
||||
// don't throw an exception, but alert the user via a log that the token was unable to be refreshed
|
||||
return;
|
||||
// check if 400 error
|
||||
} else if (
|
||||
refreshAccessToken &&
|
||||
serverResponse.status &&
|
||||
serverResponse.status >= HttpStatus.CLIENT_ERROR_RANGE_START &&
|
||||
serverResponse.status <= HttpStatus.CLIENT_ERROR_RANGE_END
|
||||
) {
|
||||
this.logger.warning(
|
||||
`executeTokenRequest:validateTokenResponse - AAD is currently available but is unable to refresh the access token.\n${serverError}`
|
||||
);
|
||||
|
||||
// don't throw an exception, but alert the user via a log that the token was unable to be refreshed
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isInteractionRequiredError(
|
||||
serverResponse.error,
|
||||
serverResponse.error_description,
|
||||
serverResponse.suberror
|
||||
)
|
||||
) {
|
||||
throw new InteractionRequiredAuthError(
|
||||
serverResponse.error,
|
||||
serverResponse.error_description,
|
||||
serverResponse.suberror,
|
||||
serverResponse.timestamp || Constants.EMPTY_STRING,
|
||||
serverResponse.trace_id || Constants.EMPTY_STRING,
|
||||
serverResponse.correlation_id || Constants.EMPTY_STRING,
|
||||
serverResponse.claims || Constants.EMPTY_STRING,
|
||||
serverErrorNo
|
||||
);
|
||||
}
|
||||
|
||||
throw serverError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a constructed token response based on given string. Also manages the cache updates and cleanups.
|
||||
* @param serverTokenResponse
|
||||
* @param authority
|
||||
*/
|
||||
async handleServerTokenResponse(
|
||||
serverTokenResponse: ServerAuthorizationTokenResponse,
|
||||
authority: Authority,
|
||||
reqTimestamp: number,
|
||||
request: BaseAuthRequest,
|
||||
authCodePayload?: AuthorizationCodePayload,
|
||||
userAssertionHash?: string,
|
||||
handlingRefreshTokenResponse?: boolean,
|
||||
forceCacheRefreshTokenResponse?: boolean,
|
||||
serverRequestId?: string
|
||||
): Promise<AuthenticationResult> {
|
||||
this.performanceClient?.addQueueMeasurement(
|
||||
PerformanceEvents.HandleServerTokenResponse,
|
||||
serverTokenResponse.correlation_id
|
||||
);
|
||||
|
||||
// create an idToken object (not entity)
|
||||
let idTokenClaims: TokenClaims | undefined;
|
||||
if (serverTokenResponse.id_token) {
|
||||
idTokenClaims = extractTokenClaims(
|
||||
serverTokenResponse.id_token || Constants.EMPTY_STRING,
|
||||
this.cryptoObj.base64Decode
|
||||
);
|
||||
|
||||
// token nonce check (TODO: Add a warning if no nonce is given?)
|
||||
if (authCodePayload && authCodePayload.nonce) {
|
||||
if (idTokenClaims.nonce !== authCodePayload.nonce) {
|
||||
throw createClientAuthError(
|
||||
ClientAuthErrorCodes.nonceMismatch
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// token max_age check
|
||||
if (request.maxAge || request.maxAge === 0) {
|
||||
const authTime = idTokenClaims.auth_time;
|
||||
if (!authTime) {
|
||||
throw createClientAuthError(
|
||||
ClientAuthErrorCodes.authTimeNotFound
|
||||
);
|
||||
}
|
||||
|
||||
checkMaxAge(authTime, request.maxAge);
|
||||
}
|
||||
}
|
||||
|
||||
// generate homeAccountId
|
||||
this.homeAccountIdentifier = AccountEntity.generateHomeAccountId(
|
||||
serverTokenResponse.client_info || Constants.EMPTY_STRING,
|
||||
authority.authorityType,
|
||||
this.logger,
|
||||
this.cryptoObj,
|
||||
idTokenClaims
|
||||
);
|
||||
|
||||
// save the response tokens
|
||||
let requestStateObj: RequestStateObject | undefined;
|
||||
if (!!authCodePayload && !!authCodePayload.state) {
|
||||
requestStateObj = ProtocolUtils.parseRequestState(
|
||||
this.cryptoObj,
|
||||
authCodePayload.state
|
||||
);
|
||||
}
|
||||
|
||||
// Add keyId from request to serverTokenResponse if defined
|
||||
serverTokenResponse.key_id =
|
||||
serverTokenResponse.key_id || request.sshKid || undefined;
|
||||
|
||||
const cacheRecord = this.generateCacheRecord(
|
||||
serverTokenResponse,
|
||||
authority,
|
||||
reqTimestamp,
|
||||
request,
|
||||
idTokenClaims,
|
||||
userAssertionHash,
|
||||
authCodePayload
|
||||
);
|
||||
let cacheContext;
|
||||
try {
|
||||
if (this.persistencePlugin && this.serializableCache) {
|
||||
this.logger.verbose(
|
||||
"Persistence enabled, calling beforeCacheAccess"
|
||||
);
|
||||
cacheContext = new TokenCacheContext(
|
||||
this.serializableCache,
|
||||
true
|
||||
);
|
||||
await this.persistencePlugin.beforeCacheAccess(cacheContext);
|
||||
}
|
||||
/*
|
||||
* When saving a refreshed tokens to the cache, it is expected that the account that was used is present in the cache.
|
||||
* If not present, we should return null, as it's the case that another application called removeAccount in between
|
||||
* the calls to getAllAccounts and acquireTokenSilent. We should not overwrite that removal, unless explicitly flagged by
|
||||
* the developer, as in the case of refresh token flow used in ADAL Node to MSAL Node migration.
|
||||
*/
|
||||
if (
|
||||
handlingRefreshTokenResponse &&
|
||||
!forceCacheRefreshTokenResponse &&
|
||||
cacheRecord.account
|
||||
) {
|
||||
const key = cacheRecord.account.generateAccountKey();
|
||||
const account = this.cacheStorage.getAccount(
|
||||
key,
|
||||
request.correlationId,
|
||||
this.logger
|
||||
);
|
||||
if (!account) {
|
||||
this.logger.warning(
|
||||
"Account used to refresh tokens not in persistence, refreshed tokens will not be stored in the cache"
|
||||
);
|
||||
return await ResponseHandler.generateAuthenticationResult(
|
||||
this.cryptoObj,
|
||||
authority,
|
||||
cacheRecord,
|
||||
false,
|
||||
request,
|
||||
idTokenClaims,
|
||||
requestStateObj,
|
||||
undefined,
|
||||
serverRequestId
|
||||
);
|
||||
}
|
||||
}
|
||||
await this.cacheStorage.saveCacheRecord(
|
||||
cacheRecord,
|
||||
request.correlationId,
|
||||
request.storeInCache
|
||||
);
|
||||
} finally {
|
||||
if (
|
||||
this.persistencePlugin &&
|
||||
this.serializableCache &&
|
||||
cacheContext
|
||||
) {
|
||||
this.logger.verbose(
|
||||
"Persistence enabled, calling afterCacheAccess"
|
||||
);
|
||||
await this.persistencePlugin.afterCacheAccess(cacheContext);
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseHandler.generateAuthenticationResult(
|
||||
this.cryptoObj,
|
||||
authority,
|
||||
cacheRecord,
|
||||
false,
|
||||
request,
|
||||
idTokenClaims,
|
||||
requestStateObj,
|
||||
serverTokenResponse,
|
||||
serverRequestId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates CacheRecord
|
||||
* @param serverTokenResponse
|
||||
* @param idTokenObj
|
||||
* @param authority
|
||||
*/
|
||||
private generateCacheRecord(
|
||||
serverTokenResponse: ServerAuthorizationTokenResponse,
|
||||
authority: Authority,
|
||||
reqTimestamp: number,
|
||||
request: BaseAuthRequest,
|
||||
idTokenClaims?: TokenClaims,
|
||||
userAssertionHash?: string,
|
||||
authCodePayload?: AuthorizationCodePayload
|
||||
): CacheRecord {
|
||||
const env = authority.getPreferredCache();
|
||||
if (!env) {
|
||||
throw createClientAuthError(
|
||||
ClientAuthErrorCodes.invalidCacheEnvironment
|
||||
);
|
||||
}
|
||||
|
||||
const claimsTenantId = getTenantIdFromIdTokenClaims(idTokenClaims);
|
||||
|
||||
// IdToken: non AAD scenarios can have empty realm
|
||||
let cachedIdToken: IdTokenEntity | undefined;
|
||||
let cachedAccount: AccountEntity | undefined;
|
||||
if (serverTokenResponse.id_token && !!idTokenClaims) {
|
||||
cachedIdToken = CacheHelpers.createIdTokenEntity(
|
||||
this.homeAccountIdentifier,
|
||||
env,
|
||||
serverTokenResponse.id_token,
|
||||
this.clientId,
|
||||
claimsTenantId || ""
|
||||
);
|
||||
|
||||
cachedAccount = buildAccountToCache(
|
||||
this.cacheStorage,
|
||||
authority,
|
||||
this.homeAccountIdentifier,
|
||||
this.cryptoObj.base64Decode,
|
||||
request.correlationId,
|
||||
idTokenClaims,
|
||||
serverTokenResponse.client_info,
|
||||
env,
|
||||
claimsTenantId,
|
||||
authCodePayload,
|
||||
undefined, // nativeAccountId
|
||||
this.logger
|
||||
);
|
||||
}
|
||||
|
||||
// AccessToken
|
||||
let cachedAccessToken: AccessTokenEntity | null = null;
|
||||
if (serverTokenResponse.access_token) {
|
||||
// If scopes not returned in server response, use request scopes
|
||||
const responseScopes = serverTokenResponse.scope
|
||||
? ScopeSet.fromString(serverTokenResponse.scope)
|
||||
: new ScopeSet(request.scopes || []);
|
||||
|
||||
/*
|
||||
* Use timestamp calculated before request
|
||||
* Server may return timestamps as strings, parse to numbers if so.
|
||||
*/
|
||||
const expiresIn: number =
|
||||
(typeof serverTokenResponse.expires_in === "string"
|
||||
? parseInt(serverTokenResponse.expires_in, 10)
|
||||
: serverTokenResponse.expires_in) || 0;
|
||||
const extExpiresIn: number =
|
||||
(typeof serverTokenResponse.ext_expires_in === "string"
|
||||
? parseInt(serverTokenResponse.ext_expires_in, 10)
|
||||
: serverTokenResponse.ext_expires_in) || 0;
|
||||
const refreshIn: number | undefined =
|
||||
(typeof serverTokenResponse.refresh_in === "string"
|
||||
? parseInt(serverTokenResponse.refresh_in, 10)
|
||||
: serverTokenResponse.refresh_in) || undefined;
|
||||
const tokenExpirationSeconds = reqTimestamp + expiresIn;
|
||||
const extendedTokenExpirationSeconds =
|
||||
tokenExpirationSeconds + extExpiresIn;
|
||||
const refreshOnSeconds =
|
||||
refreshIn && refreshIn > 0
|
||||
? reqTimestamp + refreshIn
|
||||
: undefined;
|
||||
|
||||
// non AAD scenarios can have empty realm
|
||||
cachedAccessToken = CacheHelpers.createAccessTokenEntity(
|
||||
this.homeAccountIdentifier,
|
||||
env,
|
||||
serverTokenResponse.access_token,
|
||||
this.clientId,
|
||||
claimsTenantId || authority.tenant || "",
|
||||
responseScopes.printScopes(),
|
||||
tokenExpirationSeconds,
|
||||
extendedTokenExpirationSeconds,
|
||||
this.cryptoObj.base64Decode,
|
||||
refreshOnSeconds,
|
||||
serverTokenResponse.token_type,
|
||||
userAssertionHash,
|
||||
serverTokenResponse.key_id,
|
||||
request.claims,
|
||||
request.requestedClaimsHash
|
||||
);
|
||||
}
|
||||
|
||||
// refreshToken
|
||||
let cachedRefreshToken: RefreshTokenEntity | null = null;
|
||||
if (serverTokenResponse.refresh_token) {
|
||||
let rtExpiresOn: number | undefined;
|
||||
if (serverTokenResponse.refresh_token_expires_in) {
|
||||
const rtExpiresIn: number =
|
||||
typeof serverTokenResponse.refresh_token_expires_in ===
|
||||
"string"
|
||||
? parseInt(
|
||||
serverTokenResponse.refresh_token_expires_in,
|
||||
10
|
||||
)
|
||||
: serverTokenResponse.refresh_token_expires_in;
|
||||
rtExpiresOn = reqTimestamp + rtExpiresIn;
|
||||
}
|
||||
cachedRefreshToken = CacheHelpers.createRefreshTokenEntity(
|
||||
this.homeAccountIdentifier,
|
||||
env,
|
||||
serverTokenResponse.refresh_token,
|
||||
this.clientId,
|
||||
serverTokenResponse.foci,
|
||||
userAssertionHash,
|
||||
rtExpiresOn
|
||||
);
|
||||
}
|
||||
|
||||
// appMetadata
|
||||
let cachedAppMetadata: AppMetadataEntity | null = null;
|
||||
if (serverTokenResponse.foci) {
|
||||
cachedAppMetadata = {
|
||||
clientId: this.clientId,
|
||||
environment: env,
|
||||
familyId: serverTokenResponse.foci,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
account: cachedAccount,
|
||||
idToken: cachedIdToken,
|
||||
accessToken: cachedAccessToken,
|
||||
refreshToken: cachedRefreshToken,
|
||||
appMetadata: cachedAppMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an @AuthenticationResult from @CacheRecord , @IdToken , and a boolean that states whether or not the result is from cache.
|
||||
*
|
||||
* Optionally takes a state string that is set as-is in the response.
|
||||
*
|
||||
* @param cacheRecord
|
||||
* @param idTokenObj
|
||||
* @param fromTokenCache
|
||||
* @param stateString
|
||||
*/
|
||||
static async generateAuthenticationResult(
|
||||
cryptoObj: ICrypto,
|
||||
authority: Authority,
|
||||
cacheRecord: CacheRecord,
|
||||
fromTokenCache: boolean,
|
||||
request: BaseAuthRequest,
|
||||
idTokenClaims?: TokenClaims,
|
||||
requestState?: RequestStateObject,
|
||||
serverTokenResponse?: ServerAuthorizationTokenResponse,
|
||||
requestId?: string
|
||||
): Promise<AuthenticationResult> {
|
||||
let accessToken: string = Constants.EMPTY_STRING;
|
||||
let responseScopes: Array<string> = [];
|
||||
let expiresOn: Date | null = null;
|
||||
let extExpiresOn: Date | undefined;
|
||||
let refreshOn: Date | undefined;
|
||||
let familyId: string = Constants.EMPTY_STRING;
|
||||
|
||||
if (cacheRecord.accessToken) {
|
||||
/*
|
||||
* if the request object has `popKid` property, `signPopToken` will be set to false and
|
||||
* the token will be returned unsigned
|
||||
*/
|
||||
if (
|
||||
cacheRecord.accessToken.tokenType ===
|
||||
AuthenticationScheme.POP &&
|
||||
!request.popKid
|
||||
) {
|
||||
const popTokenGenerator: PopTokenGenerator =
|
||||
new PopTokenGenerator(cryptoObj);
|
||||
const { secret, keyId } = cacheRecord.accessToken;
|
||||
|
||||
if (!keyId) {
|
||||
throw createClientAuthError(
|
||||
ClientAuthErrorCodes.keyIdMissing
|
||||
);
|
||||
}
|
||||
|
||||
accessToken = await popTokenGenerator.signPopToken(
|
||||
secret,
|
||||
keyId,
|
||||
request
|
||||
);
|
||||
} else {
|
||||
accessToken = cacheRecord.accessToken.secret;
|
||||
}
|
||||
responseScopes = ScopeSet.fromString(
|
||||
cacheRecord.accessToken.target
|
||||
).asArray();
|
||||
expiresOn = new Date(
|
||||
Number(cacheRecord.accessToken.expiresOn) * 1000
|
||||
);
|
||||
extExpiresOn = new Date(
|
||||
Number(cacheRecord.accessToken.extendedExpiresOn) * 1000
|
||||
);
|
||||
if (cacheRecord.accessToken.refreshOn) {
|
||||
refreshOn = new Date(
|
||||
Number(cacheRecord.accessToken.refreshOn) * 1000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (cacheRecord.appMetadata) {
|
||||
familyId =
|
||||
cacheRecord.appMetadata.familyId === THE_FAMILY_ID
|
||||
? THE_FAMILY_ID
|
||||
: "";
|
||||
}
|
||||
const uid = idTokenClaims?.oid || idTokenClaims?.sub || "";
|
||||
const tid = idTokenClaims?.tid || "";
|
||||
|
||||
// for hybrid + native bridge enablement, send back the native account Id
|
||||
if (serverTokenResponse?.spa_accountid && !!cacheRecord.account) {
|
||||
cacheRecord.account.nativeAccountId =
|
||||
serverTokenResponse?.spa_accountid;
|
||||
}
|
||||
|
||||
const accountInfo: AccountInfo | null = cacheRecord.account
|
||||
? updateAccountTenantProfileData(
|
||||
cacheRecord.account.getAccountInfo(),
|
||||
undefined, // tenantProfile optional
|
||||
idTokenClaims,
|
||||
cacheRecord.idToken?.secret
|
||||
)
|
||||
: null;
|
||||
|
||||
return {
|
||||
authority: authority.canonicalAuthority,
|
||||
uniqueId: uid,
|
||||
tenantId: tid,
|
||||
scopes: responseScopes,
|
||||
account: accountInfo,
|
||||
idToken: cacheRecord?.idToken?.secret || "",
|
||||
idTokenClaims: idTokenClaims || {},
|
||||
accessToken: accessToken,
|
||||
fromCache: fromTokenCache,
|
||||
expiresOn: expiresOn,
|
||||
extExpiresOn: extExpiresOn,
|
||||
refreshOn: refreshOn,
|
||||
correlationId: request.correlationId,
|
||||
requestId: requestId || Constants.EMPTY_STRING,
|
||||
familyId: familyId,
|
||||
tokenType:
|
||||
cacheRecord.accessToken?.tokenType || Constants.EMPTY_STRING,
|
||||
state: requestState
|
||||
? requestState.userRequestState
|
||||
: Constants.EMPTY_STRING,
|
||||
cloudGraphHostName:
|
||||
cacheRecord.account?.cloudGraphHostName ||
|
||||
Constants.EMPTY_STRING,
|
||||
msGraphHost:
|
||||
cacheRecord.account?.msGraphHost || Constants.EMPTY_STRING,
|
||||
code: serverTokenResponse?.spa_code,
|
||||
fromNativeBroker: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAccountToCache(
|
||||
cacheStorage: CacheManager,
|
||||
authority: Authority,
|
||||
homeAccountId: string,
|
||||
base64Decode: (input: string) => string,
|
||||
correlationId: string,
|
||||
idTokenClaims?: TokenClaims,
|
||||
clientInfo?: string,
|
||||
environment?: string,
|
||||
claimsTenantId?: string | null,
|
||||
authCodePayload?: AuthorizationCodePayload,
|
||||
nativeAccountId?: string,
|
||||
logger?: Logger
|
||||
): AccountEntity {
|
||||
logger?.verbose("setCachedAccount called");
|
||||
|
||||
// Check if base account is already cached
|
||||
const accountKeys = cacheStorage.getAccountKeys();
|
||||
const baseAccountKey = accountKeys.find((accountKey: string) => {
|
||||
return accountKey.startsWith(homeAccountId);
|
||||
});
|
||||
|
||||
let cachedAccount: AccountEntity | null = null;
|
||||
if (baseAccountKey) {
|
||||
cachedAccount = cacheStorage.getAccount(
|
||||
baseAccountKey,
|
||||
correlationId,
|
||||
logger
|
||||
);
|
||||
}
|
||||
|
||||
const baseAccount =
|
||||
cachedAccount ||
|
||||
AccountEntity.createAccount(
|
||||
{
|
||||
homeAccountId,
|
||||
idTokenClaims,
|
||||
clientInfo,
|
||||
environment,
|
||||
cloudGraphHostName: authCodePayload?.cloud_graph_host_name,
|
||||
msGraphHost: authCodePayload?.msgraph_host,
|
||||
nativeAccountId: nativeAccountId,
|
||||
},
|
||||
authority,
|
||||
base64Decode
|
||||
);
|
||||
|
||||
const tenantProfiles = baseAccount.tenantProfiles || [];
|
||||
const tenantId = claimsTenantId || baseAccount.realm;
|
||||
if (
|
||||
tenantId &&
|
||||
!tenantProfiles.find((tenantProfile) => {
|
||||
return tenantProfile.tenantId === tenantId;
|
||||
})
|
||||
) {
|
||||
const newTenantProfile = buildTenantProfile(
|
||||
homeAccountId,
|
||||
baseAccount.localAccountId,
|
||||
tenantId,
|
||||
idTokenClaims
|
||||
);
|
||||
tenantProfiles.push(newTenantProfile);
|
||||
}
|
||||
baseAccount.tenantProfiles = tenantProfiles;
|
||||
|
||||
return baseAccount;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Deserialized response object from server authorization code request.
|
||||
* - code: authorization code from server
|
||||
* - client_info: client info object
|
||||
* - state: OAuth2 request state
|
||||
* - error: error sent back in hash
|
||||
* - error: description
|
||||
*/
|
||||
export type ServerAuthorizationCodeResponse = {
|
||||
// Success case
|
||||
code?: string;
|
||||
client_info?: string;
|
||||
state?: string;
|
||||
cloud_instance_name?: string;
|
||||
cloud_instance_host_name?: string;
|
||||
cloud_graph_host_name?: string;
|
||||
msgraph_host?: string;
|
||||
// Error case
|
||||
error?: string;
|
||||
error_uri?: string;
|
||||
error_description?: string;
|
||||
suberror?: string;
|
||||
timestamp?: string;
|
||||
trace_id?: string;
|
||||
correlation_id?: string;
|
||||
claims?: string;
|
||||
// Native Account ID
|
||||
accountId?: string;
|
||||
};
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
import { AuthenticationScheme } from "../utils/Constants.js";
|
||||
|
||||
/**
|
||||
* Deserialized response object from server authorization code request.
|
||||
* - token_type: Indicates the token type value. Can be either Bearer or pop.
|
||||
* - scope: The scopes that the access_token is valid for.
|
||||
* - expires_in: How long the access token is valid (in seconds).
|
||||
* - refresh_in: Duration afer which a token should be renewed, regardless of expiration.
|
||||
* - ext_expires_in: How long the access token is valid (in seconds) if the server isn't responding.
|
||||
* - access_token: The requested access token. The app can use this token to authenticate to the secured resource, such as a web API.
|
||||
* - refresh_token: An OAuth 2.0 refresh token. The app can use this token acquire additional access tokens after the current access token expires.
|
||||
* - id_token: A JSON Web Token (JWT). The app can decode the segments of this token to request information about the user who signed in.
|
||||
* - key_id: A string that uniquely identifies a public key that the request is bound to.
|
||||
*
|
||||
* In case of error:
|
||||
* - error: An error code string that can be used to classify types of errors that occur, and can be used to react to errors.
|
||||
* - error_description: A specific error message that can help a developer identify the root cause of an authentication error.
|
||||
* - error_codes: A list of STS-specific error codes that can help in diagnostics.
|
||||
* - timestamp: The time at which the error occurred.
|
||||
* - trace_id: A unique identifier for the request that can help in diagnostics.
|
||||
* - correlation_id: A unique identifier for the request that can help in diagnostics across components.
|
||||
* - status: the network request's response status
|
||||
*/
|
||||
export type ServerAuthorizationTokenResponse = {
|
||||
status?: number;
|
||||
// Success
|
||||
token_type?: AuthenticationScheme;
|
||||
scope?: string;
|
||||
expires_in?: number;
|
||||
refresh_in?: number;
|
||||
ext_expires_in?: number;
|
||||
access_token?: string;
|
||||
refresh_token?: string;
|
||||
refresh_token_expires_in?: number;
|
||||
id_token?: string;
|
||||
client_info?: string;
|
||||
foci?: string;
|
||||
spa_code?: string;
|
||||
spa_accountid?: string;
|
||||
key_id?: string;
|
||||
// Error
|
||||
error?: string;
|
||||
error_description?: string;
|
||||
error_codes?: Array<string>;
|
||||
suberror?: string;
|
||||
timestamp?: string;
|
||||
trace_id?: string;
|
||||
correlation_id?: string;
|
||||
claims?: string;
|
||||
};
|
||||
Reference in New Issue
Block a user