diff --git a/web-vault-sso.patch b/web-vault-sso.patch new file mode 100644 index 00000000..dde307ad --- /dev/null +++ b/web-vault-sso.patch @@ -0,0 +1,570 @@ +Submodule jslib contains modified content +diff --git a/jslib/angular/src/components/register.component.ts b/jslib/angular/src/components/register.component.ts +index 53ec3c8..7b49db1 100644 +--- a/jslib/angular/src/components/register.component.ts ++++ b/jslib/angular/src/components/register.component.ts +@@ -24,7 +24,7 @@ export class RegisterComponent { + formPromise: Promise; + masterPasswordScore: number; + referenceData: ReferenceEventRequest; +- showTerms = true; ++ showTerms = false; + acceptPolicies: boolean = false; + + protected successRoute = 'login'; +@@ -35,7 +35,7 @@ export class RegisterComponent { + protected apiService: ApiService, protected stateService: StateService, + protected platformUtilsService: PlatformUtilsService, + protected passwordGenerationService: PasswordGenerationService) { +- this.showTerms = !platformUtilsService.isSelfHost(); ++ this.showTerms = false; + } + + get masterPasswordScoreWidth() { +@@ -69,6 +69,12 @@ export class RegisterComponent { + } + + async submit() { ++ if (typeof crypto.subtle === 'undefined') { ++ this.platformUtilsService.showToast('error', "This browser requires HTTPS to use the web vault", ++ "Check the Vaultwarden wiki for details on how to enable it"); ++ return; ++ } ++ + if (!this.acceptPolicies && this.showTerms) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('acceptPoliciesError')); +diff --git a/jslib/angular/src/components/sso.component.ts b/jslib/angular/src/components/sso.component.ts +index d4512a1..ad57f69 100644 +--- a/jslib/angular/src/components/sso.component.ts ++++ b/jslib/angular/src/components/sso.component.ts +@@ -19,6 +19,8 @@ import { Utils } from 'jslib-common/misc/utils'; + + import { AuthResult } from 'jslib-common/models/domain/authResult'; + ++import { switchMap } from 'rxjs/operators'; ++ + @Directive() + export class SsoComponent { + identifier: string; +@@ -48,13 +50,19 @@ export class SsoComponent { + + async ngOnInit() { + const queryParamsSub = this.route.queryParams.subscribe(async qParams => { +- if (qParams.code != null && qParams.state != null) { ++ // I have no idea why the qParams is empty here - I've hacked in an alternative very messily, but it works. ++ const workingParams = (new URL(window.location.href)).searchParams; ++ const workingSwap = { ++ code: workingParams.get('code'), ++ state: workingParams.get('state'), ++ }; ++ if (workingSwap.code != null && workingSwap.state != null) { + const codeVerifier = await this.storageService.get(ConstantsService.ssoCodeVerifierKey); + const state = await this.storageService.get(ConstantsService.ssoStateKey); + await this.storageService.remove(ConstantsService.ssoCodeVerifierKey); + await this.storageService.remove(ConstantsService.ssoStateKey); +- if (qParams.code != null && codeVerifier != null && state != null && this.checkState(state, qParams.state)) { +- await this.logIn(qParams.code, codeVerifier, this.getOrgIdentiferFromState(qParams.state)); ++ if (workingSwap.code != null && codeVerifier != null && state != null && this.checkState(state, workingSwap.state)) { ++ await this.logIn(workingSwap.code, codeVerifier, this.getOrgIdentiferFromState(workingSwap.state)); + } + } else if (qParams.clientId != null && qParams.redirectUri != null && qParams.state != null && + qParams.codeChallenge != null) { +@@ -122,7 +130,7 @@ export class SsoComponent { + let authorizeUrl = this.apiService.identityBaseUrl + '/connect/authorize?' + + 'client_id=' + this.clientId + '&redirect_uri=' + encodeURIComponent(this.redirectUri) + '&' + + 'response_type=code&scope=api offline_access&' + +- 'state=' + state + '&code_challenge=' + codeChallenge + '&' + ++ 'state=' + encodeURIComponent(state) + '&code_challenge=' + codeChallenge + '&' + + 'code_challenge_method=S256&response_mode=query&' + + 'domain_hint=' + encodeURIComponent(this.identifier); + +@@ -137,7 +145,7 @@ export class SsoComponent { + private async logIn(code: string, codeVerifier: string, orgIdFromState: string) { + this.loggingIn = true; + try { +- this.formPromise = this.authService.logInSso(code, codeVerifier, this.redirectUri); ++ this.formPromise = this.authService.logInSso(code, codeVerifier, this.redirectUri, orgIdFromState); + const response = await this.formPromise; + if (response.twoFactor) { + if (this.onSuccessfulLoginTwoFactorNavigate != null) { +diff --git a/jslib/common/src/abstractions/api.service.ts b/jslib/common/src/abstractions/api.service.ts +index 67131df..e75a874 100644 +--- a/jslib/common/src/abstractions/api.service.ts ++++ b/jslib/common/src/abstractions/api.service.ts +@@ -33,6 +33,7 @@ import { KeysRequest } from '../models/request/keysRequest'; + import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; + import { OrganizationImportRequest } from '../models/request/organizationImportRequest'; + import { OrganizationKeysRequest } from '../models/request/organizationKeysRequest'; ++import { OrganizationSsoUpdateRequest } from '../models/request/organizationSsoUpdateRequest'; + import { OrganizationTaxInfoUpdateRequest } from '../models/request/organizationTaxInfoUpdateRequest'; + import { OrganizationUpdateRequest } from '../models/request/organizationUpdateRequest'; + import { OrganizationUpgradeRequest } from '../models/request/organizationUpgradeRequest'; +@@ -360,6 +361,7 @@ export abstract class ApiService { + getOrganizationTaxInfo: (id: string) => Promise; + postOrganization: (request: OrganizationCreateRequest) => Promise; + putOrganization: (id: string, request: OrganizationUpdateRequest) => Promise; ++ putOrganizationSso: (id: string, request: OrganizationSsoUpdateRequest) => Promise; + putOrganizationTaxInfo: (id: string, request: OrganizationTaxInfoUpdateRequest) => Promise; + postLeaveOrganization: (id: string) => Promise; + postOrganizationLicense: (data: FormData) => Promise; +diff --git a/jslib/common/src/abstractions/auth.service.ts b/jslib/common/src/abstractions/auth.service.ts +index ac7ef04..5b1b774 100644 +--- a/jslib/common/src/abstractions/auth.service.ts ++++ b/jslib/common/src/abstractions/auth.service.ts +@@ -15,7 +15,7 @@ export abstract class AuthService { + selectedTwoFactorProviderType: TwoFactorProviderType; + + logIn: (email: string, masterPassword: string) => Promise; +- logInSso: (code: string, codeVerifier: string, redirectUrl: string) => Promise; ++ logInSso: (code: string, codeVerifier: string, redirectUrl: string, orgIdentifier: string) => Promise; + logInApiKey: (clientId: string, clientSecret: string) => Promise; + logInTwoFactor: (twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, + remember?: boolean) => Promise; +diff --git a/jslib/common/src/models/request/tokenRequest.ts b/jslib/common/src/models/request/tokenRequest.ts +index 7578012..964364f 100644 +--- a/jslib/common/src/models/request/tokenRequest.ts ++++ b/jslib/common/src/models/request/tokenRequest.ts +@@ -14,9 +14,10 @@ export class TokenRequest { + provider: TwoFactorProviderType; + remember: boolean; + device?: DeviceRequest; ++ orgIdentifier?: string; + + constructor(credentials: string[], codes: string[], clientIdClientSecret: string[], provider: TwoFactorProviderType, +- token: string, remember: boolean, device?: DeviceRequest) { ++ token: string, remember: boolean, device?: DeviceRequest, orgIdentifier?: string) { + if (credentials != null && credentials.length > 1) { + this.email = credentials[0]; + this.masterPasswordHash = credentials[1]; +@@ -28,6 +29,9 @@ export class TokenRequest { + this.clientId = clientIdClientSecret[0]; + this.clientSecret = clientIdClientSecret[1]; + } ++ if (orgIdentifier && orgIdentifier !== '') { ++ this.orgIdentifier = orgIdentifier; ++ } + this.token = token; + this.provider = provider; + this.remember = remember; +@@ -53,6 +57,7 @@ export class TokenRequest { + obj.code = this.code; + obj.code_verifier = this.codeVerifier; + obj.redirect_uri = this.redirectUri; ++ obj.org_identifier = this.orgIdentifier; + } else { + throw new Error('must provide credentials or codes'); + } +diff --git a/jslib/common/src/models/response/organizationResponse.ts b/jslib/common/src/models/response/organizationResponse.ts +index 21d8d43..896b7a6 100644 +--- a/jslib/common/src/models/response/organizationResponse.ts ++++ b/jslib/common/src/models/response/organizationResponse.ts +@@ -27,6 +27,12 @@ export class OrganizationResponse extends BaseResponse { + useApi: boolean; + useResetPassword: boolean; + hasPublicAndPrivateKeys: boolean; ++ useSso: boolean; ++ callbackPath: string; ++ signedOutCallbackPath: string; ++ authority: string; ++ clientId: string; ++ clientSecret: string; + + constructor(response: any) { + super(response); +@@ -54,5 +60,11 @@ export class OrganizationResponse extends BaseResponse { + this.useApi = this.getResponseProperty('UseApi'); + this.useResetPassword = this.getResponseProperty('UseResetPassword'); + this.hasPublicAndPrivateKeys = this.getResponseProperty('HasPublicAndPrivateKeys'); ++ this.useSso = this.getResponseProperty('UseSso'); ++ this.callbackPath = this.getResponseProperty('CallbackPath'); ++ this.signedOutCallbackPath = this.getResponseProperty('SignedOutCallbackPath'); ++ this.authority = this.getResponseProperty('Authority'); ++ this.clientId = this.getResponseProperty('ClientId'); ++ this.clientSecret = this.getResponseProperty('ClientSecret'); + } + } +diff --git a/jslib/common/src/services/api.service.ts b/jslib/common/src/services/api.service.ts +index 51c1c14..1a5b088 100644 +--- a/jslib/common/src/services/api.service.ts ++++ b/jslib/common/src/services/api.service.ts +@@ -37,6 +37,7 @@ import { KeysRequest } from '../models/request/keysRequest'; + import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; + import { OrganizationImportRequest } from '../models/request/organizationImportRequest'; + import { OrganizationKeysRequest } from '../models/request/organizationKeysRequest'; ++import { OrganizationSsoUpdateRequest } from '../models/request/organizationSsoUpdateRequest'; + import { OrganizationTaxInfoUpdateRequest } from '../models/request/organizationTaxInfoUpdateRequest'; + import { OrganizationUpdateRequest } from '../models/request/organizationUpdateRequest'; + import { OrganizationUpgradeRequest } from '../models/request/organizationUpgradeRequest'; +@@ -1158,6 +1159,11 @@ export class ApiService implements ApiServiceAbstraction { + return new OrganizationResponse(r); + } + ++ async putOrganizationSso(id: string, request: OrganizationSsoUpdateRequest): Promise { ++ const r = await this.send('PUT', '/organizations/' + id + '/sso', request, true, false); ++ return new OrganizationResponse(r); ++ } ++ + async putOrganizationTaxInfo(id: string, request: OrganizationTaxInfoUpdateRequest): Promise { + return this.send('PUT', '/organizations/' + id + '/tax', request, true, false); + } +diff --git a/jslib/common/src/services/auth.service.ts b/jslib/common/src/services/auth.service.ts +index 6536a94..6f4899c 100644 +--- a/jslib/common/src/services/auth.service.ts ++++ b/jslib/common/src/services/auth.service.ts +@@ -130,10 +130,10 @@ export class AuthService implements AuthServiceAbstraction { + key, null, null, null); + } + +- async logInSso(code: string, codeVerifier: string, redirectUrl: string): Promise { ++ async logInSso(code: string, codeVerifier: string, redirectUrl: string, orgIdentifier: string): Promise { + this.selectedTwoFactorProviderType = null; + return await this.logInHelper(null, null, null, code, codeVerifier, redirectUrl, null, null, +- null, null, null, null); ++ null, null, null, null, orgIdentifier); + } + + async logInApiKey(clientId: string, clientSecret: string): Promise { +@@ -272,7 +272,7 @@ export class AuthService implements AuthServiceAbstraction { + + private async logInHelper(email: string, hashedPassword: string, localHashedPassword: string, code: string, + codeVerifier: string, redirectUrl: string, clientId: string, clientSecret: string, key: SymmetricCryptoKey, +- twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean): Promise { ++ twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean, orgIdentifier?: string): Promise { + const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); + const appId = await this.appIdService.getAppId(); + const deviceRequest = new DeviceRequest(appId, this.platformUtilsService); +@@ -300,13 +300,13 @@ export class AuthService implements AuthServiceAbstraction { + let request: TokenRequest; + if (twoFactorToken != null && twoFactorProvider != null) { + request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, twoFactorProvider, +- twoFactorToken, remember, deviceRequest); ++ twoFactorToken, remember, deviceRequest, orgIdentifier); + } else if (storedTwoFactorToken != null) { + request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, TwoFactorProviderType.Remember, +- storedTwoFactorToken, false, deviceRequest); ++ storedTwoFactorToken, false, deviceRequest, orgIdentifier); + } else { + request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, null, +- null, false, deviceRequest); ++ null, false, deviceRequest, orgIdentifier); + } + + const response = await this.apiService.postIdentityToken(request); +diff --git a/src/404.html b/src/404.html +index eba36375..cb8883ec 100644 +--- a/src/404.html ++++ b/src/404.html +@@ -41,10 +41,10 @@ + +

+

You can return to the web vault, check our status page +- or contact us.

++ or contact us.

+ + + + +diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts +index bee8416f..dad32467 100644 +--- a/src/app/app-routing.module.ts ++++ b/src/app/app-routing.module.ts +@@ -33,6 +33,7 @@ import { AccountComponent as OrgAccountComponent } from './organizations/setting + import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component'; + import { OrganizationSubscriptionComponent } from './organizations/settings/organization-subscription.component'; + import { SettingsComponent as OrgSettingsComponent } from './organizations/settings/settings.component'; ++import { SsoComponent as OrgSsoComponent } from './organizations/settings/sso.component'; + import { + TwoFactorSetupComponent as OrgTwoFactorSetupComponent, + } from './organizations/settings/two-factor-setup.component'; +@@ -412,6 +413,7 @@ const routes: Routes = [ + children: [ + { path: '', pathMatch: 'full', redirectTo: 'account' }, + { path: 'account', component: OrgAccountComponent, data: { titleId: 'myOrganization' } }, ++ { path: 'sso', component: OrgSsoComponent, data: { titleId: 'sso' } }, + { path: 'two-factor', component: OrgTwoFactorSetupComponent, data: { titleId: 'twoStepLogin' } }, + { + path: 'billing', +diff --git a/src/app/app.component.ts b/src/app/app.component.ts +index 2922cf09..8f2be1ad 100644 +--- a/src/app/app.component.ts ++++ b/src/app/app.component.ts +@@ -146,6 +146,10 @@ export class AppComponent implements OnDestroy, OnInit { + } + break; + case 'showToast': ++ if (typeof message.text === "string" && typeof crypto.subtle === 'undefined') { ++ message.title="This browser requires HTTPS to use the web vault"; ++ message.text="Check the Vaultwarden wiki for details on how to enable it"; ++ } + this.showToast(message); + break; + case 'setFullWidth': +diff --git a/src/app/app.module.ts b/src/app/app.module.ts +index bafc22a0..938db7ee 100644 +--- a/src/app/app.module.ts ++++ b/src/app/app.module.ts +@@ -67,6 +67,7 @@ import { DownloadLicenseComponent } from './organizations/settings/download-lice + import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component'; + import { OrganizationSubscriptionComponent } from './organizations/settings/organization-subscription.component'; + import { SettingsComponent as OrgSettingComponent } from './organizations/settings/settings.component'; ++import { SsoComponent as OrgSsoComponent } from './organizations/settings/sso.component'; + import { + TwoFactorSetupComponent as OrgTwoFactorSetupComponent, + } from './organizations/settings/two-factor-setup.component'; +@@ -347,6 +348,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); + NavbarComponent, + OptionsComponent, + OrgAccountComponent, ++ OrgSsoComponent, + OrgAddEditComponent, + OrganizationBillingComponent, + OrganizationPlansComponent, +diff --git a/src/app/layouts/footer.component.html b/src/app/layouts/footer.component.html +index b001b9e3..c1bd2ac8 100644 +--- a/src/app/layouts/footer.component.html ++++ b/src/app/layouts/footer.component.html +@@ -1,7 +1,7 @@ +