Skip to content

Commit

Permalink
Store encrypted profile information with no way to access it without …
Browse files Browse the repository at this point in the history
…the key which is sent to client and never stored on the server (#3469)

* generate a per code/token encryption key and send it to the client, store the encrypted value so that PII exposure of storing profile is dropped to zero

* lint fix

* fixed tests

* debug

* fixed missing stub

* fixed token check

* added --enable-source-maps

* fixed flag

* removed wrong flag

* fixed tests

* tweaked stubbing of randomBytes
  • Loading branch information
deepakprabhakara authored Jan 3, 2025
1 parent 24405ae commit 416fd09
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 63 deletions.
58 changes: 52 additions & 6 deletions npm/src/controller/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,30 @@ import { SSOHandler } from './sso-handler';
import { ValidateOption, extractSAMLResponseAttributes } from '../saml/lib';
import { oidcClientConfig } from './oauth/oidc-client';
import { App } from '../ee/identity-federation/app';
import * as encrypter from '../db/encrypter';
import { Encrypted } from '../typings';

const deflateRawAsync = promisify(deflateRaw);

function encrypt(val: any) {
const genKey = crypto.randomBytes(32);
const hexKey = genKey.toString('hex');
const encVal = encrypter.encrypt(JSON.stringify(val), genKey);
return {
hexKey,
encVal,
};
}

function decrypt(res: Encrypted, encryptionKey: string) {
const encKey = Buffer.from(encryptionKey, 'hex');
if (res.iv && res.tag) {
return JSON.parse(encrypter.decrypt(res.value, res.iv, res.tag, encKey));
}

return JSON.parse(res.value);
}

export class OAuthController implements IOAuthController {
private connectionStore: Storable;
private sessionStore: Storable;
Expand Down Expand Up @@ -1053,9 +1074,11 @@ export class OAuthController implements IOAuthController {
codeVal['session'] = session;
}

await this.codeStore.put(code, codeVal);
const { hexKey, encVal } = encrypt(codeVal);

return code;
await this.codeStore.put(code, encVal);

return hexKey + '.' + code;
}

/**
Expand Down Expand Up @@ -1149,7 +1172,18 @@ export class OAuthController implements IOAuthController {
throw new JacksonError('Please specify code', 400);
}

const codeVal = await this.codeStore.get(code);
const codes = code.split('.');
if (codes.length !== 2) {
throw new JacksonError('Invalid code', 403);
}

const encCodeVal = await this.codeStore.get(codes[1]);
if (!encCodeVal) {
throw new JacksonError('Invalid code', 403);
}

const codeVal = decrypt(encCodeVal, codes[0]);

if (!codeVal || !codeVal.profile) {
throw new JacksonError('Invalid code', 403);
}
Expand Down Expand Up @@ -1256,7 +1290,9 @@ export class OAuthController implements IOAuthController {
tokenVal.claims.sub = codeVal.profile.claims.id;
}

await this.tokenStore.put(token, tokenVal);
const { hexKey, encVal } = encrypt(tokenVal);

await this.tokenStore.put(token, encVal);

// delete the code
try {
Expand All @@ -1267,7 +1303,7 @@ export class OAuthController implements IOAuthController {
}

const tokenResponse: OAuthTokenRes = {
access_token: token,
access_token: hexKey + '.' + token,
token_type: 'bearer',
expires_in: this.opts.db.ttl!,
};
Expand Down Expand Up @@ -1331,7 +1367,17 @@ export class OAuthController implements IOAuthController {
* }
*/
public async userInfo(token: string): Promise<Profile> {
const rsp = await this.tokenStore.get(token);
const tokens = token.split('.');
if (tokens.length !== 2) {
throw new JacksonError('Invalid token', 403);
}

const encRsp = await this.tokenStore.get(tokens[1]);
if (!encRsp) {
throw new JacksonError('Invalid token', 403);
}

const rsp = decrypt(encRsp, tokens[0]);

metrics.increment('oauthUserInfo');

Expand Down
35 changes: 21 additions & 14 deletions npm/test/sso/fixture.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import crypto from 'crypto';
import {
OAuthReqBody,
OAuthReqBodyWithAccessType,
Expand All @@ -11,6 +12,13 @@ import boxyhqNoentityID from './data/metadata/noentityID/boxyhq-noentityID';
import exampleOidc from './data/metadata/example.oidc';
import invalidssodescriptor from './data/metadata/invalidSSODescriptor/invalidssodescriptor';

export const code = '1234567890';
export const token = '24c1550190dd6a5a9bd6fe2a8ff69d593121c7b9';
export const genKey = crypto.randomBytes(32);
export const iv = crypto.randomBytes(12);
export const clientCode = genKey.toString('hex') + '.' + code;
export const clientToken = genKey.toString('hex') + '.' + token;

// BEGIN: Fixtures for authorize
export const authz_request_normal: Partial<OAuthReqBodyWithClientId> = {
redirect_uri: boxyhq.defaultRedirectUrl,
Expand Down Expand Up @@ -97,7 +105,7 @@ export const invalid_tenant_product = (product?, tenant?): Partial<OAuthTokenReq
grant_type: 'authorization_code',
client_id: `tenant=${tenant}&product=${product}`,
client_secret: 'dummy',
code: CODE,
code: clientCode,
redirect_uri: boxyhq.defaultRedirectUrl,
};
};
Expand Down Expand Up @@ -125,7 +133,6 @@ export const oidc_response_with_error: CallbackParamsType = {
// END: Fixtures for oidcAuthzResponse

// BEGIN: Fixtures for token
const CODE = '1234567890';
const CLIENT_SECRET_VERIFIER = 'TOP-SECRET';
export const bodyWithInvalidCode: Partial<OAuthTokenReq> = {
grant_type: 'authorization_code',
Expand All @@ -139,21 +146,21 @@ export const bodyWithInvalidRedirectUri: Partial<OAuthTokenReq> = {
grant_type: 'authorization_code',
client_id: `tenant=${boxyhq.tenant}&product=${boxyhq.product}`,
client_secret: CLIENT_SECRET_VERIFIER,
code: CODE,
code: clientCode,
redirect_uri: 'http://example.com',
};
export const bodyWithMissingRedirectUri: Partial<OAuthTokenReq> = {
grant_type: 'authorization_code',
client_id: `tenant=${boxyhq.tenant}&product=${boxyhq.product}`,
client_secret: CLIENT_SECRET_VERIFIER,
code: CODE,
code: clientCode,
};
//encoded clientId and wrong secret
export const bodyWithInvalidClientSecret: Partial<OAuthTokenReq> = {
grant_type: 'authorization_code',
client_id: `tenant=${boxyhq.tenant}&product=${boxyhq.product}`,
client_secret: 'dummy',
code: CODE,
code: clientCode,
redirect_uri: boxyhq.defaultRedirectUrl,
};
//unencoded clientId with wrong secret
Expand All @@ -165,7 +172,7 @@ export const bodyWithUnencodedClientId_InvalidClientSecret_gen = (connectionReco
grant_type: 'authorization_code',
client_id: connectionRecord.clientID,
client_secret: 'dummy',
code: CODE,
code: clientCode,
redirect_uri: boxyhq.defaultRedirectUrl,
};
};
Expand All @@ -174,15 +181,15 @@ export const bodyWithDummyCredentials: Partial<OAuthTokenReq> = {
grant_type: 'authorization_code',
client_id: `dummy`,
client_secret: 'dummy',
code: CODE,
code: clientCode,
redirect_uri: boxyhq.defaultRedirectUrl,
};

export const token_req_encoded_client_id: Partial<OAuthTokenReq> = {
grant_type: 'authorization_code',
client_id: `tenant=${boxyhq.tenant}&product=${boxyhq.product}`,
client_secret: CLIENT_SECRET_VERIFIER,
code: CODE,
code: clientCode,
redirect_uri: boxyhq.defaultRedirectUrl,
};

Expand All @@ -194,42 +201,42 @@ export const token_req_unencoded_client_id_gen = (connectionRecords) => {
grant_type: 'authorization_code',
client_id: connectionRecord.clientID,
client_secret: connectionRecord.clientSecret,
code: CODE,
code: clientCode,
redirect_uri: boxyhq.defaultRedirectUrl,
};
};

export const token_req_dummy_client_id_idp_saml_login_wrong_secretverifier = {
grant_type: 'authorization_code',
code: CODE,
code: clientCode,
client_id: 'dummy',
client_secret: 'TOP-SECRET-WRONG',
};

export const token_req_dummy_client_id_idp_saml_login = {
grant_type: 'authorization_code',
code: CODE,
code: clientCode,
client_id: 'dummy',
client_secret: 'TOP-SECRET',
};

export const token_req_encoded_client_id_idp_saml_login = {
grant_type: 'authorization_code',
code: CODE,
code: clientCode,
client_id: 'tenant=boxyhq.com&product=crm',
client_secret: 'TOP-SECRET',
};

export const token_req_encoded_client_id_idp_saml_login_wrong_secretverifier = {
grant_type: 'authorization_code',
code: CODE,
code: clientCode,
client_id: 'tenant=boxyhq.com&product=crm',
client_secret: 'TOP-SECRET-WRONG',
};

export const token_req = {
grant_type: 'authorization_code',
code: CODE,
code: clientCode,
redirect_uri: boxyhq.defaultRedirectUrl,
};

Expand Down
Loading

0 comments on commit 416fd09

Please sign in to comment.