msal
Version:
Microsoft Authentication Library for js
362 lines (317 loc) • 12.3 kB
text/typescript
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import { Authority } from "./Authority";
import { CryptoUtils } from "./utils/CryptoUtils";
import { AuthenticationParameters, validateClaimsRequest } from "./AuthenticationParameters";
import { StringDict } from "./MsalTypes"
import { Account } from "./Account";
import { SSOTypes, Constants, PromptState, BlacklistedEQParams, libraryVersion } from "./utils/Constants";
import { ClientConfigurationError } from "./error/ClientConfigurationError";
import { StringUtils } from "./utils/StringUtils";
/**
* Nonce: OIDC Nonce definition: https://openid.net/specs/openid-connect-core-1_0.html#IDToken
* State: OAuth Spec: https://tools.ietf.org/html/rfc6749#section-10.12
* @hidden
*/
export class ServerRequestParameters {
authorityInstance: Authority;
clientId: string;
scopes: Array<string>;
nonce: string;
state: string;
// telemetry information
xClientVer: string;
xClientSku: string;
correlationId: string;
responseType: string;
redirectUri: string;
promptValue: string;
claimsValue: string;
queryParameters: string;
extraQueryParameters: string;
public get authority(): string {
return this.authorityInstance ? this.authorityInstance.CanonicalAuthority : null;
}
/**
* Constructor
* @param authority
* @param clientId
* @param scope
* @param responseType
* @param redirectUri
* @param state
*/
constructor (authority: Authority, clientId: string, scope: Array<string>, responseType: string, redirectUri: string, state: string) {
this.authorityInstance = authority;
this.clientId = clientId;
if (!scope) {
this.scopes = [clientId];
} else {
this.scopes = [ ...scope ];
}
this.nonce = CryptoUtils.createNewGuid();
this.state = state && !StringUtils.isEmpty(state) ? CryptoUtils.createNewGuid() + "|" + state : CryptoUtils.createNewGuid();
// TODO: Change this to user passed vs generated with the new PR
this.correlationId = CryptoUtils.createNewGuid();
// telemetry information
this.xClientSku = "MSAL.JS";
this.xClientVer = libraryVersion();
this.responseType = responseType;
this.redirectUri = redirectUri;
}
/**
* @hidden
* @ignore
*
* Utility to populate QueryParameters and ExtraQueryParameters to ServerRequestParamerers
* @param request
* @param serverAuthenticationRequest
*/
populateQueryParams(account: Account, request: AuthenticationParameters, adalIdTokenObject?: any): void {
let queryParameters: StringDict = {};
if (request) {
// add the prompt parameter to serverRequestParameters if passed
if (request.prompt) {
this.validatePromptParameter(request.prompt);
this.promptValue = request.prompt;
}
// Add claims challenge to serverRequestParameters if passed
if (request.claimsRequest) {
validateClaimsRequest(request);
this.claimsValue = request.claimsRequest;
}
// if the developer provides one of these, give preference to developer choice
if (ServerRequestParameters.isSSOParam(request)) {
queryParameters = this.constructUnifiedCacheQueryParameter(request, null);
}
}
if (adalIdTokenObject) {
queryParameters = this.constructUnifiedCacheQueryParameter(null, adalIdTokenObject);
}
// adds sid/login_hint if not populated; populates domain_req, login_req and domain_hint
// this.logger.verbose("Calling addHint parameters");
queryParameters = this.addHintParameters(account, queryParameters);
// sanity check for developer passed extraQueryParameters
let eQParams: StringDict;
if (request) {
eQParams = this.sanitizeEQParams(request);
}
// Populate the extraQueryParameters to be sent to the server
this.queryParameters = ServerRequestParameters.generateQueryParametersString(queryParameters);
this.extraQueryParameters = ServerRequestParameters.generateQueryParametersString(eQParams);
}
//#region QueryParam helpers
/**
* @hidden
* @ignore
*
* Utility to test if valid prompt value is passed in the request
* @param request
*/
private validatePromptParameter (prompt: string) {
if (!([PromptState.LOGIN, PromptState.SELECT_ACCOUNT, PromptState.CONSENT, PromptState.NONE].indexOf(prompt) >= 0)) {
throw ClientConfigurationError.createInvalidPromptError(prompt);
}
}
/**
* Constructs extraQueryParameters to be sent to the server for the AuthenticationParameters set by the developer
* in any login() or acquireToken() calls
* @param idTokenObject
* @param extraQueryParameters
* @param sid
* @param loginHint
*/
//TODO: check how this behaves when domain_hint only is sent in extraparameters and idToken has no upn.
private constructUnifiedCacheQueryParameter(request: AuthenticationParameters, idTokenObject: any): StringDict {
// preference order: account > sid > login_hint
let ssoType;
let ssoData;
let serverReqParam: StringDict = {};
// if account info is passed, account.sid > account.login_hint
if (request) {
if (request.account) {
const account: Account = request.account;
if (account.sid) {
ssoType = SSOTypes.SID;
ssoData = account.sid;
}
else if (account.userName) {
ssoType = SSOTypes.LOGIN_HINT;
ssoData = account.userName;
}
}
// sid from request
else if (request.sid) {
ssoType = SSOTypes.SID;
ssoData = request.sid;
}
// loginHint from request
else if (request.loginHint) {
ssoType = SSOTypes.LOGIN_HINT;
ssoData = request.loginHint;
}
}
// adalIdToken retrieved from cache
else if (idTokenObject) {
if (idTokenObject.hasOwnProperty(Constants.upn)) {
ssoType = SSOTypes.ID_TOKEN;
ssoData = idTokenObject.upn;
}
else {
ssoType = SSOTypes.ORGANIZATIONS;
ssoData = null;
}
}
serverReqParam = this.addSSOParameter(ssoType, ssoData);
// add the HomeAccountIdentifier info/ domain_hint
if (request && request.account && request.account.homeAccountIdentifier) {
serverReqParam = this.addSSOParameter(SSOTypes.HOMEACCOUNT_ID, request.account.homeAccountIdentifier, serverReqParam);
}
return serverReqParam;
}
/**
* @hidden
*
* Adds login_hint to authorization URL which is used to pre-fill the username field of sign in page for the user if known ahead of time
* domain_hint can be one of users/organizations which when added skips the email based discovery process of the user
* domain_req utid received as part of the clientInfo
* login_req uid received as part of clientInfo
* Also does a sanity check for extraQueryParameters passed by the user to ensure no repeat queryParameters
*
* @param {@link Account} account - Account for which the token is requested
* @param queryparams
* @param {@link ServerRequestParameters}
* @ignore
*/
private addHintParameters(account: Account, qParams: StringDict): StringDict {
// This is a final check for all queryParams added so far; preference order: sid > login_hint
// sid cannot be passed along with login_hint or domain_hint, hence we check both are not populated yet in queryParameters
if (account && !qParams[SSOTypes.SID]) {
// sid - populate only if login_hint is not already populated and the account has sid
const populateSID = !qParams[SSOTypes.LOGIN_HINT] && account.sid && this.promptValue === PromptState.NONE;
if (populateSID) {
qParams = this.addSSOParameter(SSOTypes.SID, account.sid, qParams);
}
// login_hint - account.userName
else {
const populateLoginHint = !qParams[SSOTypes.LOGIN_HINT] && account.userName && !StringUtils.isEmpty(account.userName);
if (populateLoginHint) {
qParams = this.addSSOParameter(SSOTypes.LOGIN_HINT, account.userName, qParams);
}
}
const populateReqParams = !qParams[SSOTypes.DOMAIN_REQ] && !qParams[SSOTypes.LOGIN_REQ];
if (populateReqParams) {
qParams = this.addSSOParameter(SSOTypes.HOMEACCOUNT_ID, account.homeAccountIdentifier, qParams);
}
}
return qParams;
}
/**
* Add SID to extraQueryParameters
* @param sid
*/
private addSSOParameter(ssoType: string, ssoData: string, ssoParam?: StringDict): StringDict {
if (!ssoParam) {
ssoParam = {};
}
if (!ssoData) {
return ssoParam;
}
switch (ssoType) {
case SSOTypes.SID: {
ssoParam[SSOTypes.SID] = ssoData;
break;
}
case SSOTypes.ID_TOKEN: {
ssoParam[SSOTypes.LOGIN_HINT] = ssoData;
ssoParam[SSOTypes.DOMAIN_HINT] = SSOTypes.ORGANIZATIONS;
break;
}
case SSOTypes.LOGIN_HINT: {
ssoParam[SSOTypes.LOGIN_HINT] = ssoData;
break;
}
case SSOTypes.ORGANIZATIONS: {
ssoParam[SSOTypes.DOMAIN_HINT] = SSOTypes.ORGANIZATIONS;
break;
}
case SSOTypes.CONSUMERS: {
ssoParam[SSOTypes.DOMAIN_HINT] = SSOTypes.CONSUMERS;
break;
}
case SSOTypes.HOMEACCOUNT_ID: {
let homeAccountId = ssoData.split(".");
const uid = CryptoUtils.base64Decode(homeAccountId[0]);
const utid = CryptoUtils.base64Decode(homeAccountId[1]);
// TODO: domain_req and login_req are not needed according to eSTS team
ssoParam[SSOTypes.LOGIN_REQ] = uid;
ssoParam[SSOTypes.DOMAIN_REQ] = utid;
if (utid === Constants.consumersUtid) {
ssoParam[SSOTypes.DOMAIN_HINT] = SSOTypes.CONSUMERS;
}
else {
ssoParam[SSOTypes.DOMAIN_HINT] = SSOTypes.ORGANIZATIONS;
}
break;
}
case SSOTypes.LOGIN_REQ: {
ssoParam[SSOTypes.LOGIN_REQ] = ssoData;
break;
}
case SSOTypes.DOMAIN_REQ: {
ssoParam[SSOTypes.DOMAIN_REQ] = ssoData;
break;
}
}
return ssoParam;
}
/**
* @hidden
* @ignore
* Removes unnecessary or duplicate query parameters from extraQueryParameters
* @param request
*/
private sanitizeEQParams(request: AuthenticationParameters) : StringDict {
let eQParams : StringDict = request.extraQueryParameters;
if (!eQParams) {
return null;
}
if (request.claimsRequest) {
// this.logger.warning("Removed duplicate claims from extraQueryParameters. Please use either the claimsRequest field OR pass as extraQueryParameter - not both.");
delete eQParams[Constants.claims];
}
BlacklistedEQParams.forEach(param => {
if (eQParams[param]) {
// this.logger.warning("Removed duplicate " + param + " from extraQueryParameters. Please use the " + param + " field in request object.");
delete eQParams[param];
}
});
return eQParams;
}
/**
* Utility to generate a QueryParameterString from a Key-Value mapping of extraQueryParameters passed
* @param extraQueryParameters
*/
static generateQueryParametersString(queryParameters: StringDict): string {
let paramsString: string = null;
if (queryParameters) {
Object.keys(queryParameters).forEach((key: string) => {
if (paramsString == null) {
paramsString = `${key}=${encodeURIComponent(queryParameters[key])}`;
}
else {
paramsString += `&${key}=${encodeURIComponent(queryParameters[key])}`;
}
});
}
return paramsString;
}
//#endregion
/**
* Check to see if there are SSO params set in the Request
* @param request
*/
static isSSOParam(request: AuthenticationParameters) {
return request && (request.account || request.sid || request.loginHint);
}
}