1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-19 11:15:21 +01:00

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
This commit is contained in:
Addison Beck 2020-08-27 11:00:05 -04:00 committed by GitHub
parent 8f27110754
commit e07526a1b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 60 additions and 12 deletions

View File

@ -293,6 +293,9 @@ export abstract class ApiService {
start: string, end: string, token: string) => Promise<ListResponse<EventResponse>>; start: string, end: string, token: string) => Promise<ListResponse<EventResponse>>;
postEventsCollect: (request: EventRequest[]) => Promise<any>; postEventsCollect: (request: EventRequest[]) => Promise<any>;
deleteSsoUser: (organizationId: string) => Promise<any>;
getSsoUserIdentifier: () => Promise<string>;
getUserPublicKey: (id: string) => Promise<UserKeyResponse>; getUserPublicKey: (id: string) => Promise<UserKeyResponse>;
getHibpBreach: (username: string) => Promise<BreachAccountResponse[]>; getHibpBreach: (username: string) => Promise<BreachAccountResponse[]>;

View File

@ -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<string> {
let codeChallenge = this.codeChallenge;
let state = this.state;
const passwordOptions: any = { const passwordOptions: any = {
type: 'password', type: 'password',
length: 64, length: 64,
@ -75,26 +83,36 @@ export class SsoComponent {
numbers: true, numbers: true,
special: false, special: false,
}; };
let codeChallenge = this.codeChallenge;
let state = this.state;
if (codeChallenge == null) { if (codeChallenge == null) {
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256'); const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256');
codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
await this.storageService.save(ConstantsService.ssoCodeVerifierKey, codeVerifier); await this.storageService.save(ConstantsService.ssoCodeVerifierKey, codeVerifier);
} }
if (state == null) { if (state == null) {
state = await this.passwordGenerationService.generatePassword(passwordOptions); state = await this.passwordGenerationService.generatePassword(passwordOptions);
if (returnUri) {
state += `_returnUri='${returnUri}'`;
}
await this.storageService.save(ConstantsService.ssoStateKey, state); 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) + '&' + 'client_id=' + this.clientId + '&redirect_uri=' + encodeURIComponent(this.redirectUri) + '&' +
'response_type=code&scope=api offline_access&' + 'response_type=code&scope=api offline_access&' +
'state=' + state + '&code_challenge=' + codeChallenge + '&' + 'state=' + state + '&code_challenge=' + codeChallenge + '&' +
'code_challenge_method=S256&response_mode=query&' + 'code_challenge_method=S256&response_mode=query&' +
'domain_hint=' + encodeURIComponent(this.identifier); '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) { private async logIn(code: string, codeVerifier: string) {

View File

@ -17,11 +17,14 @@ export class OrganizationData {
use2fa: boolean; use2fa: boolean;
useApi: boolean; useApi: boolean;
useBusinessPortal: boolean; useBusinessPortal: boolean;
useSso: boolean;
selfHost: boolean; selfHost: boolean;
usersGetPremium: boolean; usersGetPremium: boolean;
seats: number; seats: number;
maxCollections: number; maxCollections: number;
maxStorageGb?: number; maxStorageGb?: number;
ssoBound: boolean;
identifier: string;
constructor(response: ProfileOrganizationResponse) { constructor(response: ProfileOrganizationResponse) {
this.id = response.id; this.id = response.id;
@ -37,10 +40,13 @@ export class OrganizationData {
this.use2fa = response.use2fa; this.use2fa = response.use2fa;
this.useApi = response.useApi; this.useApi = response.useApi;
this.useBusinessPortal = response.useBusinessPortal; this.useBusinessPortal = response.useBusinessPortal;
this.useSso = response.useSso;
this.selfHost = response.selfHost; this.selfHost = response.selfHost;
this.usersGetPremium = response.usersGetPremium; this.usersGetPremium = response.usersGetPremium;
this.seats = response.seats; this.seats = response.seats;
this.maxCollections = response.maxCollections; this.maxCollections = response.maxCollections;
this.maxStorageGb = response.maxStorageGb; this.maxStorageGb = response.maxStorageGb;
this.ssoBound = response.ssoBound;
this.identifier = response.identifier;
} }
} }

View File

@ -17,11 +17,14 @@ export class Organization {
use2fa: boolean; use2fa: boolean;
useApi: boolean; useApi: boolean;
useBusinessPortal: boolean; useBusinessPortal: boolean;
useSso: boolean;
selfHost: boolean; selfHost: boolean;
usersGetPremium: boolean; usersGetPremium: boolean;
seats: number; seats: number;
maxCollections: number; maxCollections: number;
maxStorageGb?: number; maxStorageGb?: number;
ssoBound: boolean;
identifier: string;
constructor(obj?: OrganizationData) { constructor(obj?: OrganizationData) {
if (obj == null) { if (obj == null) {
@ -41,11 +44,14 @@ export class Organization {
this.use2fa = obj.use2fa; this.use2fa = obj.use2fa;
this.useApi = obj.useApi; this.useApi = obj.useApi;
this.useBusinessPortal = obj.useBusinessPortal; this.useBusinessPortal = obj.useBusinessPortal;
this.useSso = obj.useSso;
this.selfHost = obj.selfHost; this.selfHost = obj.selfHost;
this.usersGetPremium = obj.usersGetPremium; this.usersGetPremium = obj.usersGetPremium;
this.seats = obj.seats; this.seats = obj.seats;
this.maxCollections = obj.maxCollections; this.maxCollections = obj.maxCollections;
this.maxStorageGb = obj.maxStorageGb; this.maxStorageGb = obj.maxStorageGb;
this.ssoBound = obj.ssoBound;
this.identifier = obj.identifier;
} }
get canAccess() { get canAccess() {

View File

@ -14,6 +14,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
use2fa: boolean; use2fa: boolean;
useApi: boolean; useApi: boolean;
useBusinessPortal: boolean; useBusinessPortal: boolean;
useSso: boolean;
selfHost: boolean; selfHost: boolean;
usersGetPremium: boolean; usersGetPremium: boolean;
seats: number; seats: number;
@ -23,6 +24,8 @@ export class ProfileOrganizationResponse extends BaseResponse {
status: OrganizationUserStatusType; status: OrganizationUserStatusType;
type: OrganizationUserType; type: OrganizationUserType;
enabled: boolean; enabled: boolean;
ssoBound: boolean;
identifier: string;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
@ -36,6 +39,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
this.use2fa = this.getResponseProperty('Use2fa'); this.use2fa = this.getResponseProperty('Use2fa');
this.useApi = this.getResponseProperty('UseApi'); this.useApi = this.getResponseProperty('UseApi');
this.useBusinessPortal = this.getResponseProperty('UseBusinessPortal'); this.useBusinessPortal = this.getResponseProperty('UseBusinessPortal');
this.useSso = this.getResponseProperty('UseSso');
this.selfHost = this.getResponseProperty('SelfHost'); this.selfHost = this.getResponseProperty('SelfHost');
this.usersGetPremium = this.getResponseProperty('UsersGetPremium'); this.usersGetPremium = this.getResponseProperty('UsersGetPremium');
this.seats = this.getResponseProperty('Seats'); this.seats = this.getResponseProperty('Seats');
@ -45,5 +49,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
this.status = this.getResponseProperty('Status'); this.status = this.getResponseProperty('Status');
this.type = this.getResponseProperty('Type'); this.type = this.getResponseProperty('Type');
this.enabled = this.getResponseProperty('Enabled'); this.enabled = this.getResponseProperty('Enabled');
this.ssoBound = this.getResponseProperty('SsoBound');
this.identifier = this.getResponseProperty('Identifier');
} }
} }

View File

@ -348,6 +348,14 @@ export class ApiService implements ApiServiceAbstraction {
return r as string; return r as string;
} }
async deleteSsoUser(organizationId: string): Promise<any> {
return this.send('DELETE', '/accounts/sso/' + organizationId, null, true, false);
}
async getSsoUserIdentifier(): Promise<string> {
return this.send('GET', '/accounts/sso/user-identifier', null, true, true)
}
// Folder APIs // Folder APIs
async getFolder(id: string): Promise<FolderResponse> { async getFolder(id: string): Promise<FolderResponse> {
@ -693,13 +701,6 @@ export class ApiService implements ApiServiceAbstraction {
return new ListResponse(r, PlanResponse); return new ListResponse(r, PlanResponse);
} }
// Sync APIs
async getSync(): Promise<SyncResponse> {
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<any> { async postImportDirectory(organizationId: string, request: ImportDirectoryRequest): Promise<any> {
return this.send('POST', '/organizations/' + organizationId + '/import', request, true, false); return this.send('POST', '/organizations/' + organizationId + '/import', request, true, false);
@ -717,6 +718,14 @@ export class ApiService implements ApiServiceAbstraction {
return new DomainsResponse(r); return new DomainsResponse(r);
} }
// Sync APIs
async getSync(): Promise<SyncResponse> {
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 // Two-factor APIs
async getTwoFactorProviders(): Promise<ListResponse<TwoFactorProviderResponse>> { async getTwoFactorProviders(): Promise<ListResponse<TwoFactorProviderResponse>> {