feat: init and fork some code from angular-auth-oidc-client
Some checks are pending
Build, Lint & Test Lib / Built, Lint and Test Library (push) Waiting to run
Build, Lint & Test Lib / Angular latest (push) Blocked by required conditions
Build, Lint & Test Lib / Angular latest & Schematics Job (push) Blocked by required conditions
Build, Lint & Test Lib / Angular latest Standalone & Schematics Job (push) Blocked by required conditions
Build, Lint & Test Lib / Angular 16 & RxJs 6 (push) Blocked by required conditions
Build, Lint & Test Lib / Angular V16 (push) Blocked by required conditions
Docs / Build and Deploy Docs Job (push) Waiting to run
Docs / Close Pull Request Job (push) Waiting to run
Some checks are pending
Build, Lint & Test Lib / Built, Lint and Test Library (push) Waiting to run
Build, Lint & Test Lib / Angular latest (push) Blocked by required conditions
Build, Lint & Test Lib / Angular latest & Schematics Job (push) Blocked by required conditions
Build, Lint & Test Lib / Angular latest Standalone & Schematics Job (push) Blocked by required conditions
Build, Lint & Test Lib / Angular 16 & RxJs 6 (push) Blocked by required conditions
Build, Lint & Test Lib / Angular V16 (push) Blocked by required conditions
Docs / Build and Deploy Docs Job (push) Waiting to run
Docs / Close Pull Request Job (push) Waiting to run
This commit is contained in:
192
src/config/validation/config-validation.service.spec.ts
Normal file
192
src/config/validation/config-validation.service.spec.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { mockProvider } from '../../../test/auto-mock';
|
||||
import { LogLevel } from '../../logging/log-level';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { OpenIdConfiguration } from '../openid-configuration';
|
||||
import { ConfigValidationService } from './config-validation.service';
|
||||
import { allMultipleConfigRules } from './rules';
|
||||
|
||||
describe('Config Validation Service', () => {
|
||||
let configValidationService: ConfigValidationService;
|
||||
let loggerService: LoggerService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [ConfigValidationService, mockProvider(LoggerService)],
|
||||
});
|
||||
});
|
||||
|
||||
const VALID_CONFIG = {
|
||||
authority: 'https://offeringsolutions-sts.azurewebsites.net',
|
||||
redirectUrl: window.location.origin,
|
||||
postLogoutRedirectUri: window.location.origin,
|
||||
clientId: 'angularClient',
|
||||
scope: 'openid profile email',
|
||||
responseType: 'code',
|
||||
silentRenew: true,
|
||||
silentRenewUrl: `${window.location.origin}/silent-renew.html`,
|
||||
renewTimeBeforeTokenExpiresInSeconds: 10,
|
||||
logLevel: LogLevel.Debug,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
configValidationService = TestBed.inject(ConfigValidationService);
|
||||
loggerService = TestBed.inject(LoggerService);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(configValidationService).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false for empty config', () => {
|
||||
const config = {};
|
||||
const result = configValidationService.validateConfig(config);
|
||||
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
|
||||
it('should return true for valid config', () => {
|
||||
const result = configValidationService.validateConfig(VALID_CONFIG);
|
||||
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
|
||||
it('calls `logWarning` if one rule has warning level', () => {
|
||||
const loggerWarningSpy = spyOn(loggerService, 'logWarning');
|
||||
const messageTypeSpy = spyOn(
|
||||
configValidationService as any,
|
||||
'getAllMessagesOfType'
|
||||
);
|
||||
|
||||
messageTypeSpy
|
||||
.withArgs('warning', jasmine.any(Array))
|
||||
.and.returnValue(['A warning message']);
|
||||
messageTypeSpy.withArgs('error', jasmine.any(Array)).and.callThrough();
|
||||
|
||||
configValidationService.validateConfig(VALID_CONFIG);
|
||||
expect(loggerWarningSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('ensure-clientId.rule', () => {
|
||||
it('return false when no clientId is set', () => {
|
||||
const config = { ...VALID_CONFIG, clientId: '' } as OpenIdConfiguration;
|
||||
const result = configValidationService.validateConfig(config);
|
||||
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensure-authority-server.rule', () => {
|
||||
it('return false when no security token service is set', () => {
|
||||
const config = {
|
||||
...VALID_CONFIG,
|
||||
authority: '',
|
||||
} as OpenIdConfiguration;
|
||||
const result = configValidationService.validateConfig(config);
|
||||
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensure-redirect-url.rule', () => {
|
||||
it('return false for no redirect Url', () => {
|
||||
const config = { ...VALID_CONFIG, redirectUrl: '' };
|
||||
const result = configValidationService.validateConfig(config);
|
||||
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureSilentRenewUrlWhenNoRefreshTokenUsed', () => {
|
||||
it('return false when silent renew is used with no useRefreshToken and no silentrenewUrl', () => {
|
||||
const config = {
|
||||
...VALID_CONFIG,
|
||||
silentRenew: true,
|
||||
useRefreshToken: false,
|
||||
silentRenewUrl: '',
|
||||
} as OpenIdConfiguration;
|
||||
const result = configValidationService.validateConfig(config);
|
||||
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('use-offline-scope-with-silent-renew.rule', () => {
|
||||
it('return true but warning when silent renew is used with useRefreshToken but no offline_access scope is given', () => {
|
||||
const config = {
|
||||
...VALID_CONFIG,
|
||||
silentRenew: true,
|
||||
useRefreshToken: true,
|
||||
scopes: 'scope1 scope2 but_no_offline_access',
|
||||
};
|
||||
|
||||
const loggerSpy = spyOn(loggerService, 'logError');
|
||||
const loggerWarningSpy = spyOn(loggerService, 'logWarning');
|
||||
|
||||
const result = configValidationService.validateConfig(config);
|
||||
|
||||
expect(result).toBeTrue();
|
||||
expect(loggerSpy).not.toHaveBeenCalled();
|
||||
expect(loggerWarningSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensure-no-duplicated-configs.rule', () => {
|
||||
it('should print out correct error when mutiple configs with same properties are passed', () => {
|
||||
const config1 = {
|
||||
...VALID_CONFIG,
|
||||
silentRenew: true,
|
||||
useRefreshToken: true,
|
||||
scopes: 'scope1 scope2 but_no_offline_access',
|
||||
};
|
||||
const config2 = {
|
||||
...VALID_CONFIG,
|
||||
silentRenew: true,
|
||||
useRefreshToken: true,
|
||||
scopes: 'scope1 scope2 but_no_offline_access',
|
||||
};
|
||||
|
||||
const loggerErrorSpy = spyOn(loggerService, 'logError');
|
||||
const loggerWarningSpy = spyOn(loggerService, 'logWarning');
|
||||
|
||||
const result = configValidationService.validateConfigs([
|
||||
config1,
|
||||
config2,
|
||||
]);
|
||||
|
||||
expect(result).toBeTrue();
|
||||
expect(loggerErrorSpy).not.toHaveBeenCalled();
|
||||
expect(loggerWarningSpy.calls.argsFor(0)).toEqual([
|
||||
config1,
|
||||
'You added multiple configs with the same authority, clientId and scope',
|
||||
]);
|
||||
expect(loggerWarningSpy.calls.argsFor(1)).toEqual([
|
||||
config2,
|
||||
'You added multiple configs with the same authority, clientId and scope',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return false and a better error message when config is not passed as object with config property', () => {
|
||||
const loggerWarningSpy = spyOn(loggerService, 'logWarning');
|
||||
|
||||
const result = configValidationService.validateConfigs([]);
|
||||
|
||||
expect(result).toBeFalse();
|
||||
expect(loggerWarningSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateConfigs', () => {
|
||||
it('calls internal method with empty array if something falsy is passed', () => {
|
||||
const spy = spyOn(
|
||||
configValidationService as any,
|
||||
'validateConfigsInternal'
|
||||
).and.callThrough();
|
||||
|
||||
const result = configValidationService.validateConfigs([]);
|
||||
|
||||
expect(result).toBeFalse();
|
||||
expect(spy).toHaveBeenCalledOnceWith([], allMultipleConfigRules);
|
||||
});
|
||||
});
|
||||
});
|
||||
98
src/config/validation/config-validation.service.ts
Normal file
98
src/config/validation/config-validation.service.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { OpenIdConfiguration } from '../openid-configuration';
|
||||
import { Level, RuleValidationResult } from './rule';
|
||||
import { allMultipleConfigRules, allRules } from './rules';
|
||||
|
||||
@Injectable()
|
||||
export class ConfigValidationService {
|
||||
private readonly loggerService = inject(LoggerService);
|
||||
|
||||
validateConfigs(passedConfigs: OpenIdConfiguration[]): boolean {
|
||||
return this.validateConfigsInternal(
|
||||
passedConfigs ?? [],
|
||||
allMultipleConfigRules
|
||||
);
|
||||
}
|
||||
|
||||
validateConfig(passedConfig: OpenIdConfiguration): boolean {
|
||||
return this.validateConfigInternal(passedConfig, allRules);
|
||||
}
|
||||
|
||||
private validateConfigsInternal(
|
||||
passedConfigs: OpenIdConfiguration[],
|
||||
allRulesToUse: ((
|
||||
passedConfig: OpenIdConfiguration[]
|
||||
) => RuleValidationResult)[]
|
||||
): boolean {
|
||||
if (passedConfigs.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const allValidationResults = allRulesToUse.map((rule) =>
|
||||
rule(passedConfigs)
|
||||
);
|
||||
|
||||
let overallErrorCount = 0;
|
||||
|
||||
passedConfigs.forEach((passedConfig) => {
|
||||
const errorCount = this.processValidationResultsAndGetErrorCount(
|
||||
allValidationResults,
|
||||
passedConfig
|
||||
);
|
||||
|
||||
overallErrorCount += errorCount;
|
||||
});
|
||||
|
||||
return overallErrorCount === 0;
|
||||
}
|
||||
|
||||
private validateConfigInternal(
|
||||
passedConfig: OpenIdConfiguration,
|
||||
allRulesToUse: ((
|
||||
passedConfig: OpenIdConfiguration
|
||||
) => RuleValidationResult)[]
|
||||
): boolean {
|
||||
const allValidationResults = allRulesToUse.map((rule) =>
|
||||
rule(passedConfig)
|
||||
);
|
||||
|
||||
const errorCount = this.processValidationResultsAndGetErrorCount(
|
||||
allValidationResults,
|
||||
passedConfig
|
||||
);
|
||||
|
||||
return errorCount === 0;
|
||||
}
|
||||
|
||||
private processValidationResultsAndGetErrorCount(
|
||||
allValidationResults: RuleValidationResult[],
|
||||
config: OpenIdConfiguration
|
||||
): number {
|
||||
const allMessages = allValidationResults.filter(
|
||||
(x) => x.messages.length > 0
|
||||
);
|
||||
const allErrorMessages = this.getAllMessagesOfType('error', allMessages);
|
||||
const allWarnings = this.getAllMessagesOfType('warning', allMessages);
|
||||
|
||||
allErrorMessages.forEach((message) =>
|
||||
this.loggerService.logError(config, message)
|
||||
);
|
||||
allWarnings.forEach((message) =>
|
||||
this.loggerService.logWarning(config, message)
|
||||
);
|
||||
|
||||
return allErrorMessages.length;
|
||||
}
|
||||
|
||||
private getAllMessagesOfType(
|
||||
type: Level,
|
||||
results: RuleValidationResult[]
|
||||
): string[] {
|
||||
const allMessages = results
|
||||
.filter((x) => x.level === type)
|
||||
.map((result) => result.messages);
|
||||
|
||||
return allMessages.reduce((acc, val) => acc.concat(val), []);
|
||||
}
|
||||
}
|
||||
19
src/config/validation/rule.ts
Normal file
19
src/config/validation/rule.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { OpenIdConfiguration } from '../openid-configuration';
|
||||
|
||||
export interface Rule {
|
||||
validate(passedConfig: OpenIdConfiguration): RuleValidationResult;
|
||||
}
|
||||
|
||||
export interface RuleValidationResult {
|
||||
result: boolean;
|
||||
messages: string[];
|
||||
level: Level;
|
||||
}
|
||||
|
||||
export const POSITIVE_VALIDATION_RESULT: RuleValidationResult = {
|
||||
result: true,
|
||||
messages: [],
|
||||
level: 'none',
|
||||
};
|
||||
|
||||
export type Level = 'warning' | 'error' | 'none';
|
||||
16
src/config/validation/rules/ensure-authority.rule.ts
Normal file
16
src/config/validation/rules/ensure-authority.rule.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { OpenIdConfiguration } from '../../openid-configuration';
|
||||
import { POSITIVE_VALIDATION_RESULT, RuleValidationResult } from '../rule';
|
||||
|
||||
export const ensureAuthority = (
|
||||
passedConfig: OpenIdConfiguration
|
||||
): RuleValidationResult => {
|
||||
if (!passedConfig.authority) {
|
||||
return {
|
||||
result: false,
|
||||
messages: ['The authority URL MUST be provided in the configuration! '],
|
||||
level: 'error',
|
||||
};
|
||||
}
|
||||
|
||||
return POSITIVE_VALIDATION_RESULT;
|
||||
};
|
||||
16
src/config/validation/rules/ensure-clientId.rule.ts
Normal file
16
src/config/validation/rules/ensure-clientId.rule.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { OpenIdConfiguration } from '../../openid-configuration';
|
||||
import { POSITIVE_VALIDATION_RESULT, RuleValidationResult } from '../rule';
|
||||
|
||||
export const ensureClientId = (
|
||||
passedConfig: OpenIdConfiguration
|
||||
): RuleValidationResult => {
|
||||
if (!passedConfig.clientId) {
|
||||
return {
|
||||
result: false,
|
||||
messages: ['The clientId is required and missing from your config!'],
|
||||
level: 'error',
|
||||
};
|
||||
}
|
||||
|
||||
return POSITIVE_VALIDATION_RESULT;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { OpenIdConfiguration } from '../../openid-configuration';
|
||||
import { POSITIVE_VALIDATION_RESULT, RuleValidationResult } from '../rule';
|
||||
|
||||
const createIdentifierToCheck = (passedConfig: OpenIdConfiguration): string => {
|
||||
if (!passedConfig) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const { authority, clientId, scope } = passedConfig;
|
||||
|
||||
return `${authority}${clientId}${scope}`;
|
||||
};
|
||||
|
||||
const arrayHasDuplicates = (array: string[]): boolean =>
|
||||
new Set(array).size !== array.length;
|
||||
|
||||
export const ensureNoDuplicatedConfigsRule = (
|
||||
passedConfigs: OpenIdConfiguration[]
|
||||
): RuleValidationResult => {
|
||||
const allIdentifiers = passedConfigs.map((x) => createIdentifierToCheck(x));
|
||||
|
||||
const someAreNotSet = allIdentifiers.some((x) => x === '');
|
||||
|
||||
if (someAreNotSet) {
|
||||
return {
|
||||
result: false,
|
||||
messages: [
|
||||
`Please make sure you add an object with a 'config' property: ....({ config }) instead of ...(config)`,
|
||||
],
|
||||
level: 'error',
|
||||
};
|
||||
}
|
||||
|
||||
const hasDuplicates = arrayHasDuplicates(allIdentifiers);
|
||||
|
||||
if (hasDuplicates) {
|
||||
return {
|
||||
result: false,
|
||||
messages: [
|
||||
'You added multiple configs with the same authority, clientId and scope',
|
||||
],
|
||||
level: 'warning',
|
||||
};
|
||||
}
|
||||
|
||||
return POSITIVE_VALIDATION_RESULT;
|
||||
};
|
||||
16
src/config/validation/rules/ensure-redirect-url.rule.ts
Normal file
16
src/config/validation/rules/ensure-redirect-url.rule.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { OpenIdConfiguration } from '../../openid-configuration';
|
||||
import { POSITIVE_VALIDATION_RESULT, RuleValidationResult } from '../rule';
|
||||
|
||||
export const ensureRedirectRule = (
|
||||
passedConfig: OpenIdConfiguration
|
||||
): RuleValidationResult => {
|
||||
if (!passedConfig.redirectUrl) {
|
||||
return {
|
||||
result: false,
|
||||
messages: ['The redirectUrl is required and missing from your config'],
|
||||
level: 'error',
|
||||
};
|
||||
}
|
||||
|
||||
return POSITIVE_VALIDATION_RESULT;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { OpenIdConfiguration } from '../../openid-configuration';
|
||||
import { POSITIVE_VALIDATION_RESULT, RuleValidationResult } from '../rule';
|
||||
|
||||
export const ensureSilentRenewUrlWhenNoRefreshTokenUsed = (
|
||||
passedConfig: OpenIdConfiguration
|
||||
): RuleValidationResult => {
|
||||
const usesSilentRenew = passedConfig.silentRenew;
|
||||
const usesRefreshToken = passedConfig.useRefreshToken;
|
||||
const hasSilentRenewUrl = passedConfig.silentRenewUrl;
|
||||
|
||||
if (usesSilentRenew && !usesRefreshToken && !hasSilentRenewUrl) {
|
||||
return {
|
||||
result: false,
|
||||
messages: [
|
||||
'Please provide a silent renew URL if using renew and not refresh tokens',
|
||||
],
|
||||
level: 'error',
|
||||
};
|
||||
}
|
||||
|
||||
return POSITIVE_VALIDATION_RESULT;
|
||||
};
|
||||
16
src/config/validation/rules/index.ts
Normal file
16
src/config/validation/rules/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ensureAuthority } from './ensure-authority.rule';
|
||||
import { ensureClientId } from './ensure-clientId.rule';
|
||||
import { ensureNoDuplicatedConfigsRule } from './ensure-no-duplicated-configs.rule';
|
||||
import { ensureRedirectRule } from './ensure-redirect-url.rule';
|
||||
import { ensureSilentRenewUrlWhenNoRefreshTokenUsed } from './ensure-silentRenewUrl-with-no-refreshtokens.rule';
|
||||
import { useOfflineScopeWithSilentRenew } from './use-offline-scope-with-silent-renew.rule';
|
||||
|
||||
export const allRules = [
|
||||
ensureAuthority,
|
||||
useOfflineScopeWithSilentRenew,
|
||||
ensureRedirectRule,
|
||||
ensureClientId,
|
||||
ensureSilentRenewUrlWhenNoRefreshTokenUsed,
|
||||
];
|
||||
|
||||
export const allMultipleConfigRules = [ensureNoDuplicatedConfigsRule];
|
||||
@@ -0,0 +1,23 @@
|
||||
import { OpenIdConfiguration } from '../../openid-configuration';
|
||||
import { POSITIVE_VALIDATION_RESULT, RuleValidationResult } from '../rule';
|
||||
|
||||
export const useOfflineScopeWithSilentRenew = (
|
||||
passedConfig: OpenIdConfiguration
|
||||
): RuleValidationResult => {
|
||||
const hasRefreshToken = passedConfig.useRefreshToken;
|
||||
const hasSilentRenew = passedConfig.silentRenew;
|
||||
const scope = passedConfig.scope || '';
|
||||
const hasOfflineScope = scope.split(' ').includes('offline_access');
|
||||
|
||||
if (hasRefreshToken && hasSilentRenew && !hasOfflineScope) {
|
||||
return {
|
||||
result: false,
|
||||
messages: [
|
||||
'When using silent renew and refresh tokens please set the `offline_access` scope',
|
||||
],
|
||||
level: 'warning',
|
||||
};
|
||||
}
|
||||
|
||||
return POSITIVE_VALIDATION_RESULT;
|
||||
};
|
||||
Reference in New Issue
Block a user