-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for SSO login authentication (#168)
Relates to #103
- Loading branch information
Showing
21 changed files
with
335 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { requestAppApi } from '../requestApi'; | ||
|
||
interface PerformSsoVerificationPayload { | ||
/** The login of the user */ | ||
login: string; | ||
/** The SSO token */ | ||
ssoToken: string; | ||
} | ||
|
||
export interface PerformSsoVerificationBodyData { | ||
/** Authentication ticket usable several time */ | ||
authTicket: string; | ||
} | ||
|
||
export const performSSOVerification = (params: PerformSsoVerificationPayload) => | ||
requestAppApi<PerformSsoVerificationBodyData>({ | ||
path: 'authentication/PerformSsoVerification', | ||
payload: { | ||
login: params.login, | ||
ssoToken: params.ssoToken, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { chromium } from 'playwright'; | ||
import { DASHLANE_APP_REGEX, extractSsoInfoFromUrl } from './utils'; | ||
import { performSSOVerification } from '../../../endpoints/performSSOVerification'; | ||
|
||
interface SSOParams { | ||
requestedLogin: string; | ||
serviceProviderURL: string; | ||
} | ||
|
||
const openIdPAndWaitForRedirectURL = async (serviceProviderURL: string, userLogin: string): Promise<URL> => { | ||
return new Promise((resolve, reject) => { | ||
void (async () => { | ||
try { | ||
const browser = await chromium.launch({ headless: false, channel: 'chrome' }); | ||
const context = await browser.newContext(); | ||
const page = await context.newPage(); | ||
|
||
page.on('framenavigated', (frame) => { | ||
const url = page.url(); | ||
if (frame === page.mainFrame() && url.match(DASHLANE_APP_REGEX)) { | ||
void browser.close(); | ||
resolve(new URL(url)); | ||
} | ||
}); | ||
|
||
browser.on('disconnected', () => { | ||
reject(new Error('Browser closed before SSO login')); | ||
}); | ||
|
||
await page.goto(serviceProviderURL); | ||
|
||
// attempt to fill the login field | ||
await page | ||
.getByLabel('email') | ||
.or(page.getByLabel('Username')) | ||
.fill(userLogin) | ||
.catch(() => null); | ||
} catch (error) { | ||
reject(error); | ||
} | ||
})(); | ||
}); | ||
}; | ||
|
||
export const doSSOVerification = async ({ requestedLogin, serviceProviderURL }: SSOParams) => { | ||
const redirectURL = await openIdPAndWaitForRedirectURL(serviceProviderURL, requestedLogin); | ||
const ssoInfo = extractSsoInfoFromUrl(redirectURL); | ||
|
||
if (requestedLogin !== ssoInfo.login) { | ||
throw new Error('Login received from IdP does not match'); | ||
} | ||
|
||
if (ssoInfo.currentAuths !== ssoInfo.expectedAuths) { | ||
throw new Error('SSO Migration is not supported'); | ||
} | ||
|
||
const ssoVerificationResult = await performSSOVerification({ | ||
login: ssoInfo.login, | ||
ssoToken: ssoInfo.ssoToken, | ||
}); | ||
|
||
return { ...ssoVerificationResult, ssoSpKey: ssoInfo.key }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export enum SsoMigrationServerMethod { | ||
SSO = 'sso', | ||
MP = 'master_password', | ||
} | ||
|
||
export interface SSOAuthenticationInfo { | ||
login: string; | ||
ssoToken: string; | ||
key: string; | ||
exists: string; | ||
currentAuths: SsoMigrationServerMethod; | ||
expectedAuths: SsoMigrationServerMethod; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { SSOAuthenticationInfo } from './types'; | ||
|
||
export const DASHLANE_APP_REGEX = /https:\/\/app.dashlane.com/; | ||
|
||
/** | ||
* Convert a URL into SSOAuthenticationInfo structure | ||
* @param url Redirection URL provided by the IdP with required data for SSO login | ||
* @returns SSOAuthenticationInfo data structure | ||
*/ | ||
export const extractSsoInfoFromUrl = (url: URL): SSOAuthenticationInfo => { | ||
// Data is available in URL hash, within the form of search parameters. | ||
const search_params = new URLSearchParams(url.hash.substring(1)); | ||
|
||
// To be rewritten properly | ||
return Object.fromEntries( | ||
Object.entries({ | ||
login: '', | ||
ssoToken: '', | ||
key: '', | ||
exists: '', | ||
currentAuths: '', | ||
expectedAuths: '', | ||
}).map(([k]) => [k, search_params.get(k)]) | ||
) as unknown as SSOAuthenticationInfo; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { decryptAesCbcHmac256 } from './decrypt'; | ||
import { deserializeEncryptedData } from './encryptedDataDeserialization'; | ||
import { RemoteKey } from './types'; | ||
import { xor } from './xor'; | ||
|
||
interface BuildSsoRemoteKey { | ||
ssoServerKey: string | undefined; | ||
ssoSpKey: string | undefined | null; | ||
remoteKeys: RemoteKey[] | undefined; | ||
} | ||
|
||
export const decryptSsoRemoteKey = (params: BuildSsoRemoteKey) => { | ||
const { ssoServerKey, ssoSpKey, remoteKeys } = params; | ||
|
||
if (!ssoServerKey) { | ||
throw new Error('SSO server key is missing'); | ||
} | ||
if (!ssoSpKey) { | ||
throw new Error('SSO Service Provider key is missing'); | ||
} | ||
if (!remoteKeys || remoteKeys.length === 0) { | ||
throw new Error('Remote keys are missing'); | ||
} | ||
|
||
const remoteKey = remoteKeys.filter((key) => key.type === 'sso')[0]; | ||
|
||
if (!remoteKey) { | ||
throw new Error('Remote SSO key is missing'); | ||
} | ||
|
||
const ssoKeysXored = xor(Buffer.from(ssoServerKey, 'base64'), Buffer.from(ssoSpKey, 'base64')); | ||
|
||
const remoteKeyBase64 = Buffer.from(remoteKey.key, 'base64'); | ||
const decodedBase64 = remoteKeyBase64.toString('ascii'); | ||
const { encryptedData } = deserializeEncryptedData(decodedBase64, remoteKeyBase64); | ||
|
||
const decryptedRemoteKey = decryptAesCbcHmac256({ | ||
cipherData: encryptedData.cipherData, | ||
originalKey: ssoKeysXored, | ||
inflatedKey: true, | ||
}); | ||
|
||
return decryptedRemoteKey.toString('base64'); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.