mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
Improve SSO Config validation (#572)
* Extract SsoConfig enums to own file * Add ChangeStripSpaces directive * Move custom validators to jslib * Add a11y-invalid directive * Add and implement dirtyValidators * Create ssoConfigView model and factory methods * Add interface for select options * Don't build SsoConfigData if null Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>
This commit is contained in:
26
angular/src/directives/a11y-invalid.directive.ts
Normal file
26
angular/src/directives/a11y-invalid.directive.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Directive, ElementRef, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { NgControl } from "@angular/forms";
|
||||||
|
import { Subscription } from "rxjs";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "[appA11yInvalid]",
|
||||||
|
})
|
||||||
|
export class A11yInvalidDirective implements OnDestroy, OnInit {
|
||||||
|
private sub: Subscription;
|
||||||
|
|
||||||
|
constructor(private el: ElementRef<HTMLInputElement>, private formControlDirective: NgControl) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.sub = this.formControlDirective.control.statusChanges.subscribe((status) => {
|
||||||
|
if (status === "INVALID") {
|
||||||
|
this.el.nativeElement.setAttribute("aria-invalid", "true");
|
||||||
|
} else if (status === "VALID") {
|
||||||
|
this.el.nativeElement.setAttribute("aria-invalid", "false");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.sub?.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
12
angular/src/directives/input-strip-spaces.directive.ts
Normal file
12
angular/src/directives/input-strip-spaces.directive.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Directive, ElementRef, HostListener } from "@angular/core";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "input[appInputStripSpaces]",
|
||||||
|
})
|
||||||
|
export class InputStripSpacesDirective {
|
||||||
|
constructor(private el: ElementRef<HTMLInputElement>) {}
|
||||||
|
|
||||||
|
@HostListener("input") onInput() {
|
||||||
|
this.el.nativeElement.value = this.el.nativeElement.value.replace(/ /g, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
5
angular/src/interfaces/selectOptions.ts
Normal file
5
angular/src/interfaces/selectOptions.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface SelectOptions {
|
||||||
|
name: string;
|
||||||
|
value: any;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
25
angular/src/validators/dirty.validator.ts
Normal file
25
angular/src/validators/dirty.validator.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { AbstractControl, ValidationErrors, ValidatorFn, Validators } from "@angular/forms";
|
||||||
|
import { requiredIf } from "./requiredIf.validator";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A higher order function that takes a ValidatorFn and returns a new validator.
|
||||||
|
* The new validator only runs the ValidatorFn if the control is dirty. This prevents error messages from being
|
||||||
|
* displayed to the user prematurely.
|
||||||
|
*/
|
||||||
|
function dirtyValidator(validator: ValidatorFn) {
|
||||||
|
return (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
return control.dirty ? validator(control) : null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dirtyRequiredIf(predicate: (predicateCtrl: AbstractControl) => boolean) {
|
||||||
|
return dirtyValidator(requiredIf(predicate));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equivalent to dirtyValidator(Validator.required), however using dirtyValidator returns a new function
|
||||||
|
* each time which prevents formControl.hasError from properly comparing functions for equality.
|
||||||
|
*/
|
||||||
|
export function dirtyRequired(control: AbstractControl): ValidationErrors | null {
|
||||||
|
return control.dirty ? Validators.required(control) : null;
|
||||||
|
}
|
||||||
10
angular/src/validators/requiredIf.validator.ts
Normal file
10
angular/src/validators/requiredIf.validator.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { AbstractControl, ValidationErrors, Validators } from "@angular/forms";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new validator which will apply Validators.required only if the predicate is true.
|
||||||
|
*/
|
||||||
|
export function requiredIf(predicate: (predicateCtrl: AbstractControl) => boolean) {
|
||||||
|
return (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
return predicate(control) ? Validators.required(control) : null;
|
||||||
|
};
|
||||||
|
}
|
||||||
34
common/src/enums/ssoEnums.ts
Normal file
34
common/src/enums/ssoEnums.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export enum SsoType {
|
||||||
|
None = 0,
|
||||||
|
OpenIdConnect = 1,
|
||||||
|
Saml2 = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OpenIdConnectRedirectBehavior {
|
||||||
|
RedirectGet = 0,
|
||||||
|
FormPost = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Saml2BindingType {
|
||||||
|
HttpRedirect = 1,
|
||||||
|
HttpPost = 2,
|
||||||
|
Artifact = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Saml2NameIdFormat {
|
||||||
|
NotConfigured = 0,
|
||||||
|
Unspecified = 1,
|
||||||
|
EmailAddress = 2,
|
||||||
|
X509SubjectName = 3,
|
||||||
|
WindowsDomainQualifiedName = 4,
|
||||||
|
KerberosPrincipalName = 5,
|
||||||
|
EntityIdentifier = 6,
|
||||||
|
Persistent = 7,
|
||||||
|
Transient = 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Saml2SigningBehavior {
|
||||||
|
IfIdpWantAuthnRequestsSigned = 0,
|
||||||
|
Always = 1,
|
||||||
|
Never = 3,
|
||||||
|
}
|
||||||
@@ -1,40 +1,58 @@
|
|||||||
import { BaseResponse } from "../response/baseResponse";
|
import { BaseResponse } from "../response/baseResponse";
|
||||||
|
|
||||||
enum SsoType {
|
import {
|
||||||
OpenIdConnect = 1,
|
OpenIdConnectRedirectBehavior,
|
||||||
Saml2 = 2,
|
Saml2BindingType,
|
||||||
}
|
Saml2NameIdFormat,
|
||||||
|
Saml2SigningBehavior,
|
||||||
enum OpenIdConnectRedirectBehavior {
|
SsoType,
|
||||||
RedirectGet = 0,
|
} from "../../enums/ssoEnums";
|
||||||
FormPost = 1,
|
import { SsoConfigView } from "../view/ssoConfigView";
|
||||||
}
|
|
||||||
|
|
||||||
enum Saml2BindingType {
|
|
||||||
HttpRedirect = 1,
|
|
||||||
HttpPost = 2,
|
|
||||||
Artifact = 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Saml2NameIdFormat {
|
|
||||||
NotConfigured = 0,
|
|
||||||
Unspecified = 1,
|
|
||||||
EmailAddress = 2,
|
|
||||||
X509SubjectName = 3,
|
|
||||||
WindowsDomainQualifiedName = 4,
|
|
||||||
KerberosPrincipalName = 5,
|
|
||||||
EntityIdentifier = 6,
|
|
||||||
Persistent = 7,
|
|
||||||
Transient = 8,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Saml2SigningBehavior {
|
|
||||||
IfIdpWantAuthnRequestsSigned = 0,
|
|
||||||
Always = 1,
|
|
||||||
Never = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SsoConfigApi extends BaseResponse {
|
export class SsoConfigApi extends BaseResponse {
|
||||||
|
static fromView(view: SsoConfigView, api = new SsoConfigApi()) {
|
||||||
|
api.configType = view.configType;
|
||||||
|
|
||||||
|
api.keyConnectorEnabled = view.keyConnectorEnabled;
|
||||||
|
api.keyConnectorUrl = view.keyConnectorUrl;
|
||||||
|
|
||||||
|
if (api.configType === SsoType.OpenIdConnect) {
|
||||||
|
api.authority = view.openId.authority;
|
||||||
|
api.clientId = view.openId.clientId;
|
||||||
|
api.clientSecret = view.openId.clientSecret;
|
||||||
|
api.metadataAddress = view.openId.metadataAddress;
|
||||||
|
api.redirectBehavior = view.openId.redirectBehavior;
|
||||||
|
api.getClaimsFromUserInfoEndpoint = view.openId.getClaimsFromUserInfoEndpoint;
|
||||||
|
api.additionalScopes = view.openId.additionalScopes;
|
||||||
|
api.additionalUserIdClaimTypes = view.openId.additionalUserIdClaimTypes;
|
||||||
|
api.additionalEmailClaimTypes = view.openId.additionalEmailClaimTypes;
|
||||||
|
api.additionalNameClaimTypes = view.openId.additionalNameClaimTypes;
|
||||||
|
api.acrValues = view.openId.acrValues;
|
||||||
|
api.expectedReturnAcrValue = view.openId.expectedReturnAcrValue;
|
||||||
|
} else if (api.configType === SsoType.Saml2) {
|
||||||
|
api.spNameIdFormat = view.saml.spNameIdFormat;
|
||||||
|
api.spOutboundSigningAlgorithm = view.saml.spOutboundSigningAlgorithm;
|
||||||
|
api.spSigningBehavior = view.saml.spSigningBehavior;
|
||||||
|
api.spMinIncomingSigningAlgorithm = view.saml.spMinIncomingSigningAlgorithm;
|
||||||
|
api.spWantAssertionsSigned = view.saml.spWantAssertionsSigned;
|
||||||
|
api.spValidateCertificates = view.saml.spValidateCertificates;
|
||||||
|
|
||||||
|
api.idpEntityId = view.saml.idpEntityId;
|
||||||
|
api.idpBindingType = view.saml.idpBindingType;
|
||||||
|
api.idpSingleSignOnServiceUrl = view.saml.idpSingleSignOnServiceUrl;
|
||||||
|
api.idpSingleLogoutServiceUrl = view.saml.idpSingleLogoutServiceUrl;
|
||||||
|
api.idpArtifactResolutionServiceUrl = view.saml.idpArtifactResolutionServiceUrl;
|
||||||
|
api.idpX509PublicCert = view.saml.idpX509PublicCert;
|
||||||
|
api.idpOutboundSigningAlgorithm = view.saml.idpOutboundSigningAlgorithm;
|
||||||
|
api.idpAllowUnsolicitedAuthnResponse = view.saml.idpAllowUnsolicitedAuthnResponse;
|
||||||
|
api.idpWantAuthnRequestsSigned = view.saml.idpWantAuthnRequestsSigned;
|
||||||
|
|
||||||
|
// Value is inverted in the api model (disable instead of allow)
|
||||||
|
api.idpDisableOutboundLogoutRequests = !view.saml.idpAllowOutboundLogoutRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
return api;
|
||||||
|
}
|
||||||
configType: SsoType;
|
configType: SsoType;
|
||||||
|
|
||||||
keyConnectorEnabled: boolean;
|
keyConnectorEnabled: boolean;
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ export class OrganizationSsoResponse extends BaseResponse {
|
|||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
this.enabled = this.getResponseProperty("Enabled");
|
this.enabled = this.getResponseProperty("Enabled");
|
||||||
this.data = new SsoConfigApi(this.getResponseProperty("Data"));
|
this.data =
|
||||||
|
this.getResponseProperty("Data") != null
|
||||||
|
? new SsoConfigApi(this.getResponseProperty("Data"))
|
||||||
|
: null;
|
||||||
this.urls = new SsoUrls(this.getResponseProperty("Urls"));
|
this.urls = new SsoUrls(this.getResponseProperty("Urls"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
107
common/src/models/view/ssoConfigView.ts
Normal file
107
common/src/models/view/ssoConfigView.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { View } from "./view";
|
||||||
|
|
||||||
|
import { SsoConfigApi } from "../api/ssoConfigApi";
|
||||||
|
|
||||||
|
import {
|
||||||
|
OpenIdConnectRedirectBehavior,
|
||||||
|
Saml2BindingType,
|
||||||
|
Saml2NameIdFormat,
|
||||||
|
Saml2SigningBehavior,
|
||||||
|
SsoType,
|
||||||
|
} from "../../enums/ssoEnums";
|
||||||
|
|
||||||
|
export class SsoConfigView extends View {
|
||||||
|
configType: SsoType;
|
||||||
|
|
||||||
|
keyConnectorEnabled: boolean;
|
||||||
|
keyConnectorUrl: string;
|
||||||
|
|
||||||
|
openId: {
|
||||||
|
authority: string;
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
metadataAddress: string;
|
||||||
|
redirectBehavior: OpenIdConnectRedirectBehavior;
|
||||||
|
getClaimsFromUserInfoEndpoint: boolean;
|
||||||
|
additionalScopes: string;
|
||||||
|
additionalUserIdClaimTypes: string;
|
||||||
|
additionalEmailClaimTypes: string;
|
||||||
|
additionalNameClaimTypes: string;
|
||||||
|
acrValues: string;
|
||||||
|
expectedReturnAcrValue: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
saml: {
|
||||||
|
spNameIdFormat: Saml2NameIdFormat;
|
||||||
|
spOutboundSigningAlgorithm: string;
|
||||||
|
spSigningBehavior: Saml2SigningBehavior;
|
||||||
|
spMinIncomingSigningAlgorithm: boolean;
|
||||||
|
spWantAssertionsSigned: boolean;
|
||||||
|
spValidateCertificates: boolean;
|
||||||
|
|
||||||
|
idpEntityId: string;
|
||||||
|
idpBindingType: Saml2BindingType;
|
||||||
|
idpSingleSignOnServiceUrl: string;
|
||||||
|
idpSingleLogoutServiceUrl: string;
|
||||||
|
idpArtifactResolutionServiceUrl: string;
|
||||||
|
idpX509PublicCert: string;
|
||||||
|
idpOutboundSigningAlgorithm: string;
|
||||||
|
idpAllowUnsolicitedAuthnResponse: boolean;
|
||||||
|
idpAllowOutboundLogoutRequests: boolean;
|
||||||
|
idpWantAuthnRequestsSigned: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(api: SsoConfigApi) {
|
||||||
|
super();
|
||||||
|
if (api == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.configType = api.configType;
|
||||||
|
|
||||||
|
this.keyConnectorEnabled = api.keyConnectorEnabled;
|
||||||
|
this.keyConnectorUrl = api.keyConnectorUrl;
|
||||||
|
|
||||||
|
if (this.configType === SsoType.OpenIdConnect) {
|
||||||
|
this.openId = {
|
||||||
|
authority: api.authority,
|
||||||
|
clientId: api.clientId,
|
||||||
|
clientSecret: api.clientSecret,
|
||||||
|
metadataAddress: api.metadataAddress,
|
||||||
|
redirectBehavior: api.redirectBehavior,
|
||||||
|
getClaimsFromUserInfoEndpoint: api.getClaimsFromUserInfoEndpoint,
|
||||||
|
additionalScopes: api.additionalScopes,
|
||||||
|
additionalUserIdClaimTypes: api.additionalUserIdClaimTypes,
|
||||||
|
additionalEmailClaimTypes: api.additionalEmailClaimTypes,
|
||||||
|
additionalNameClaimTypes: api.additionalNameClaimTypes,
|
||||||
|
acrValues: api.acrValues,
|
||||||
|
expectedReturnAcrValue: api.expectedReturnAcrValue,
|
||||||
|
};
|
||||||
|
} else if (this.configType === SsoType.Saml2) {
|
||||||
|
this.saml = {
|
||||||
|
spNameIdFormat: api.spNameIdFormat,
|
||||||
|
spOutboundSigningAlgorithm: api.spOutboundSigningAlgorithm,
|
||||||
|
spSigningBehavior: api.spSigningBehavior,
|
||||||
|
spMinIncomingSigningAlgorithm: api.spMinIncomingSigningAlgorithm,
|
||||||
|
spWantAssertionsSigned: api.spWantAssertionsSigned,
|
||||||
|
spValidateCertificates: api.spValidateCertificates,
|
||||||
|
|
||||||
|
idpEntityId: api.idpEntityId,
|
||||||
|
idpBindingType: api.idpBindingType,
|
||||||
|
idpSingleSignOnServiceUrl: api.idpSingleSignOnServiceUrl,
|
||||||
|
idpSingleLogoutServiceUrl: api.idpSingleLogoutServiceUrl,
|
||||||
|
idpArtifactResolutionServiceUrl: api.idpArtifactResolutionServiceUrl,
|
||||||
|
idpX509PublicCert: api.idpX509PublicCert,
|
||||||
|
idpOutboundSigningAlgorithm: api.idpOutboundSigningAlgorithm,
|
||||||
|
idpAllowUnsolicitedAuthnResponse: api.idpAllowUnsolicitedAuthnResponse,
|
||||||
|
idpWantAuthnRequestsSigned: api.idpWantAuthnRequestsSigned,
|
||||||
|
|
||||||
|
// Value is inverted in the view model (allow instead of disable)
|
||||||
|
idpAllowOutboundLogoutRequests:
|
||||||
|
api.idpDisableOutboundLogoutRequests == null
|
||||||
|
? null
|
||||||
|
: !api.idpDisableOutboundLogoutRequests,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user