From e07526a1b6cae520561e452a6695a1508d785585 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Thu, 27 Aug 2020 11:00:05 -0400 Subject: [PATCH] Link existing user to sso (#158) * facilite linking an existing user to an org sso * fixed a broken import * added ssoBound and identifier to an org model * added user identifier to sso callout url * changed url for delete sso user api method * facilite linking an existing user to an org sso * fixed a broken import * added ssoBound and identifier to an org model * added user identifier to sso callout url * changed url for delete sso user api method * added a token to the existing user sso link flow * facilite linking an existing user to an org sso * fixed a broken import * facilite linking an existing user to an org sso * fixed a broken import * added ssoBound and identifier to an org model * added user identifier to sso callout url * changed url for delete sso user api method * added a token to the existing user sso link flow * facilite linking an existing user to an org sso * fixed a broken import * removed an extra line * encoded the user identifier on sso link * code review cleanup for link sso * removed a blank line --- src/abstractions/api.service.ts | 3 ++ src/angular/components/sso.component.ts | 28 +++++++++++++++---- src/models/data/organizationData.ts | 6 ++++ src/models/domain/organization.ts | 6 ++++ .../response/profileOrganizationResponse.ts | 6 ++++ src/services/api.service.ts | 23 ++++++++++----- 6 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 266c1c68ae..f670c9ca00 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -293,6 +293,9 @@ export abstract class ApiService { start: string, end: string, token: string) => Promise>; postEventsCollect: (request: EventRequest[]) => Promise; + deleteSsoUser: (organizationId: string) => Promise; + getSsoUserIdentifier: () => Promise; + getUserPublicKey: (id: string) => Promise; getHibpBreach: (username: string) => Promise; diff --git a/src/angular/components/sso.component.ts b/src/angular/components/sso.component.ts index 5b873bf4fb..563307ff75 100644 --- a/src/angular/components/sso.component.ts +++ b/src/angular/components/sso.component.ts @@ -66,7 +66,15 @@ export class SsoComponent { }); } - async submit() { + async submit(returnUri?: string, includeUserIdentifier?: boolean) { + const authorizeUrl = await this.buildAuthorizeUrl(returnUri, includeUserIdentifier); + this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true }); + } + + protected async buildAuthorizeUrl(returnUri?: string, includeUserIdentifier?: boolean): Promise { + let codeChallenge = this.codeChallenge; + let state = this.state; + const passwordOptions: any = { type: 'password', length: 64, @@ -75,26 +83,36 @@ export class SsoComponent { numbers: true, special: false, }; - let codeChallenge = this.codeChallenge; - let state = this.state; + if (codeChallenge == null) { const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256'); codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); await this.storageService.save(ConstantsService.ssoCodeVerifierKey, codeVerifier); } + if (state == null) { state = await this.passwordGenerationService.generatePassword(passwordOptions); + if (returnUri) { + state += `_returnUri='${returnUri}'`; + } + await this.storageService.save(ConstantsService.ssoStateKey, state); } - const authorizeUrl = this.apiService.identityBaseUrl + '/connect/authorize?' + + 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 + '&' + 'code_challenge_method=S256&response_mode=query&' + 'domain_hint=' + encodeURIComponent(this.identifier); - this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true }); + + if (includeUserIdentifier) { + const userIdentifier = await this.apiService.getSsoUserIdentifier(); + authorizeUrl += `&user_identifier=${encodeURIComponent(userIdentifier)}`; + } + + return authorizeUrl; } private async logIn(code: string, codeVerifier: string) { diff --git a/src/models/data/organizationData.ts b/src/models/data/organizationData.ts index 4b0fc7694c..d6d10dbbd4 100644 --- a/src/models/data/organizationData.ts +++ b/src/models/data/organizationData.ts @@ -17,11 +17,14 @@ export class OrganizationData { use2fa: boolean; useApi: boolean; useBusinessPortal: boolean; + useSso: boolean; selfHost: boolean; usersGetPremium: boolean; seats: number; maxCollections: number; maxStorageGb?: number; + ssoBound: boolean; + identifier: string; constructor(response: ProfileOrganizationResponse) { this.id = response.id; @@ -37,10 +40,13 @@ export class OrganizationData { this.use2fa = response.use2fa; this.useApi = response.useApi; this.useBusinessPortal = response.useBusinessPortal; + this.useSso = response.useSso; this.selfHost = response.selfHost; this.usersGetPremium = response.usersGetPremium; this.seats = response.seats; this.maxCollections = response.maxCollections; this.maxStorageGb = response.maxStorageGb; + this.ssoBound = response.ssoBound; + this.identifier = response.identifier; } } diff --git a/src/models/domain/organization.ts b/src/models/domain/organization.ts index 21011fb095..f616f2e1c3 100644 --- a/src/models/domain/organization.ts +++ b/src/models/domain/organization.ts @@ -17,11 +17,14 @@ export class Organization { use2fa: boolean; useApi: boolean; useBusinessPortal: boolean; + useSso: boolean; selfHost: boolean; usersGetPremium: boolean; seats: number; maxCollections: number; maxStorageGb?: number; + ssoBound: boolean; + identifier: string; constructor(obj?: OrganizationData) { if (obj == null) { @@ -41,11 +44,14 @@ export class Organization { this.use2fa = obj.use2fa; this.useApi = obj.useApi; this.useBusinessPortal = obj.useBusinessPortal; + this.useSso = obj.useSso; this.selfHost = obj.selfHost; this.usersGetPremium = obj.usersGetPremium; this.seats = obj.seats; this.maxCollections = obj.maxCollections; this.maxStorageGb = obj.maxStorageGb; + this.ssoBound = obj.ssoBound; + this.identifier = obj.identifier; } get canAccess() { diff --git a/src/models/response/profileOrganizationResponse.ts b/src/models/response/profileOrganizationResponse.ts index f14ba9714b..91a4c350d6 100644 --- a/src/models/response/profileOrganizationResponse.ts +++ b/src/models/response/profileOrganizationResponse.ts @@ -14,6 +14,7 @@ export class ProfileOrganizationResponse extends BaseResponse { use2fa: boolean; useApi: boolean; useBusinessPortal: boolean; + useSso: boolean; selfHost: boolean; usersGetPremium: boolean; seats: number; @@ -23,6 +24,8 @@ export class ProfileOrganizationResponse extends BaseResponse { status: OrganizationUserStatusType; type: OrganizationUserType; enabled: boolean; + ssoBound: boolean; + identifier: string; constructor(response: any) { super(response); @@ -36,6 +39,7 @@ export class ProfileOrganizationResponse extends BaseResponse { this.use2fa = this.getResponseProperty('Use2fa'); this.useApi = this.getResponseProperty('UseApi'); this.useBusinessPortal = this.getResponseProperty('UseBusinessPortal'); + this.useSso = this.getResponseProperty('UseSso'); this.selfHost = this.getResponseProperty('SelfHost'); this.usersGetPremium = this.getResponseProperty('UsersGetPremium'); this.seats = this.getResponseProperty('Seats'); @@ -45,5 +49,7 @@ export class ProfileOrganizationResponse extends BaseResponse { this.status = this.getResponseProperty('Status'); this.type = this.getResponseProperty('Type'); this.enabled = this.getResponseProperty('Enabled'); + this.ssoBound = this.getResponseProperty('SsoBound'); + this.identifier = this.getResponseProperty('Identifier'); } } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index f79178e2a7..22cb00f508 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -348,6 +348,14 @@ export class ApiService implements ApiServiceAbstraction { return r as string; } + async deleteSsoUser(organizationId: string): Promise { + return this.send('DELETE', '/accounts/sso/' + organizationId, null, true, false); + } + + async getSsoUserIdentifier(): Promise { + return this.send('GET', '/accounts/sso/user-identifier', null, true, true) + } + // Folder APIs async getFolder(id: string): Promise { @@ -693,13 +701,6 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, PlanResponse); } - // Sync APIs - - async getSync(): Promise { - const path = this.isDesktopClient || this.isWebClient ? '/sync?excludeDomains=true' : '/sync'; - const r = await this.send('GET', path, null, true, true); - return new SyncResponse(r); - } async postImportDirectory(organizationId: string, request: ImportDirectoryRequest): Promise { return this.send('POST', '/organizations/' + organizationId + '/import', request, true, false); @@ -717,6 +718,14 @@ export class ApiService implements ApiServiceAbstraction { return new DomainsResponse(r); } + // Sync APIs + + async getSync(): Promise { + const path = this.isDesktopClient || this.isWebClient ? '/sync?excludeDomains=true' : '/sync'; + const r = await this.send('GET', path, null, true, true); + return new SyncResponse(r); + } + // Two-factor APIs async getTwoFactorProviders(): Promise> {