UNPKG

@azure/msal-common

Version:
237 lines (212 loc) 7.18 kB
/* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { createClientConfigurationError, ClientConfigurationErrorCodes, } from "../error/ClientConfigurationError.js"; import { StringUtils } from "../utils/StringUtils.js"; import { ClientAuthErrorCodes, createClientAuthError, } from "../error/ClientAuthError.js"; import { Constants, OIDC_SCOPES } from "../utils/Constants.js"; /** * The ScopeSet class creates a set of scopes. Scopes are case-insensitive, unique values, so the Set object in JS makes * the most sense to implement for this class. All scopes are trimmed and converted to lower case strings in intersection and union functions * to ensure uniqueness of strings. */ export class ScopeSet { // Scopes as a Set of strings private scopes: Set<string>; constructor(inputScopes: Array<string>) { // Filter empty string and null/undefined array items const scopeArr = inputScopes ? StringUtils.trimArrayEntries([...inputScopes]) : []; const filteredInput = scopeArr ? StringUtils.removeEmptyStringsFromArray(scopeArr) : []; // Check if scopes array has at least one member if (!filteredInput || !filteredInput.length) { throw createClientConfigurationError( ClientConfigurationErrorCodes.emptyInputScopesError ); } this.scopes = new Set<string>(); // Iterator in constructor not supported by IE11 filteredInput.forEach((scope) => this.scopes.add(scope)); } /** * Factory method to create ScopeSet from space-delimited string * @param inputScopeString * @param appClientId * @param scopesRequired */ static fromString(inputScopeString: string): ScopeSet { const scopeString = inputScopeString || Constants.EMPTY_STRING; const inputScopes: Array<string> = scopeString.split(" "); return new ScopeSet(inputScopes); } /** * Creates the set of scopes to search for in cache lookups * @param inputScopeString * @returns */ static createSearchScopes(inputScopeString: Array<string>): ScopeSet { const scopeSet = new ScopeSet(inputScopeString); if (!scopeSet.containsOnlyOIDCScopes()) { scopeSet.removeOIDCScopes(); } else { scopeSet.removeScope(Constants.OFFLINE_ACCESS_SCOPE); } return scopeSet; } /** * Check if a given scope is present in this set of scopes. * @param scope */ containsScope(scope: string): boolean { const lowerCaseScopes = this.printScopesLowerCase().split(" "); const lowerCaseScopesSet = new ScopeSet(lowerCaseScopes); // compare lowercase scopes return scope ? lowerCaseScopesSet.scopes.has(scope.toLowerCase()) : false; } /** * Check if a set of scopes is present in this set of scopes. * @param scopeSet */ containsScopeSet(scopeSet: ScopeSet): boolean { if (!scopeSet || scopeSet.scopes.size <= 0) { return false; } return ( this.scopes.size >= scopeSet.scopes.size && scopeSet.asArray().every((scope) => this.containsScope(scope)) ); } /** * Check if set of scopes contains only the defaults */ containsOnlyOIDCScopes(): boolean { let defaultScopeCount = 0; OIDC_SCOPES.forEach((defaultScope: string) => { if (this.containsScope(defaultScope)) { defaultScopeCount += 1; } }); return this.scopes.size === defaultScopeCount; } /** * Appends single scope if passed * @param newScope */ appendScope(newScope: string): void { if (newScope) { this.scopes.add(newScope.trim()); } } /** * Appends multiple scopes if passed * @param newScopes */ appendScopes(newScopes: Array<string>): void { try { newScopes.forEach((newScope) => this.appendScope(newScope)); } catch (e) { throw createClientAuthError( ClientAuthErrorCodes.cannotAppendScopeSet ); } } /** * Removes element from set of scopes. * @param scope */ removeScope(scope: string): void { if (!scope) { throw createClientAuthError( ClientAuthErrorCodes.cannotRemoveEmptyScope ); } this.scopes.delete(scope.trim()); } /** * Removes default scopes from set of scopes * Primarily used to prevent cache misses if the default scopes are not returned from the server */ removeOIDCScopes(): void { OIDC_SCOPES.forEach((defaultScope: string) => { this.scopes.delete(defaultScope); }); } /** * Combines an array of scopes with the current set of scopes. * @param otherScopes */ unionScopeSets(otherScopes: ScopeSet): Set<string> { if (!otherScopes) { throw createClientAuthError( ClientAuthErrorCodes.emptyInputScopeSet ); } const unionScopes = new Set<string>(); // Iterator in constructor not supported in IE11 otherScopes.scopes.forEach((scope) => unionScopes.add(scope.toLowerCase()) ); this.scopes.forEach((scope) => unionScopes.add(scope.toLowerCase())); return unionScopes; } /** * Check if scopes intersect between this set and another. * @param otherScopes */ intersectingScopeSets(otherScopes: ScopeSet): boolean { if (!otherScopes) { throw createClientAuthError( ClientAuthErrorCodes.emptyInputScopeSet ); } // Do not allow OIDC scopes to be the only intersecting scopes if (!otherScopes.containsOnlyOIDCScopes()) { otherScopes.removeOIDCScopes(); } const unionScopes = this.unionScopeSets(otherScopes); const sizeOtherScopes = otherScopes.getScopeCount(); const sizeThisScopes = this.getScopeCount(); const sizeUnionScopes = unionScopes.size; return sizeUnionScopes < sizeThisScopes + sizeOtherScopes; } /** * Returns size of set of scopes. */ getScopeCount(): number { return this.scopes.size; } /** * Returns the scopes as an array of string values */ asArray(): Array<string> { const array: Array<string> = []; this.scopes.forEach((val) => array.push(val)); return array; } /** * Prints scopes into a space-delimited string */ printScopes(): string { if (this.scopes) { const scopeArr = this.asArray(); return scopeArr.join(" "); } return Constants.EMPTY_STRING; } /** * Prints scopes into a space-delimited lower-case string (used for caching) */ printScopesLowerCase(): string { return this.printScopes().toLowerCase(); } }