Skip to content

Commit

Permalink
bug(settings): Fix issue with 'scope' string format
Browse files Browse the repository at this point in the history
Because:
- Password reset flow would fail due to an invalid scope being set to the auth server's /authorization endpoint

This Commit:
- Ports code and tests from content-server's _normalizeScopesAndPermissions
  • Loading branch information
dschom authored and julianpoy committed Jul 31, 2023
1 parent 254b5a8 commit 7034e51
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 22 deletions.
16 changes: 11 additions & 5 deletions packages/fxa-settings/src/lib/reliers/relier-factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ describe('lib/reliers/relier-factory', () => {
expect(relier.isOAuth()).toBeFalsy();
expect(await relier.isSync()).toBeFalsy();
expect(relier.wantsKeys()).toBeFalsy();
expect(relier.isTrusted()).toBeTruthy();
expect(await relier.isTrusted()).toBeTruthy();
});

// TODO: Remove with approval.
Expand Down Expand Up @@ -185,7 +185,7 @@ describe('lib/reliers/relier-factory', () => {
expect(relier.isOAuth()).toBeFalsy();
expect(await relier.isSync()).toBeTruthy();
expect(relier.wantsKeys()).toBeTruthy();
expect(relier.isTrusted()).toBeTruthy();
expect(await relier.isTrusted()).toBeTruthy();
});

it('populates model from the search parameters', async () => {
Expand Down Expand Up @@ -214,14 +214,20 @@ describe('lib/reliers/relier-factory', () => {
{ initRelier: 1, initOAuthRelier: 1, initClientInfo: 1 },
(r: Relier) => r instanceof OAuthRelier
);

sandbox.stub(relier, 'clientInfo').returns(
Promise.resolve({
trusted: true,
})
);
});

it('has correct state', async () => {
expect(relier.name).toEqual('oauth');
expect(relier.isOAuth()).toBeTruthy();
expect(await relier.isSync()).toBeFalsy();
expect(relier.wantsKeys()).toBeFalsy();
expect(relier.isTrusted()).toBeFalsy();
expect(await relier.isTrusted()).toBeFalsy();
});
// TODO: Port remaining tests from content-server
});
Expand All @@ -245,7 +251,7 @@ describe('lib/reliers/relier-factory', () => {
expect(relier.isOAuth()).toBeTruthy();
expect(await relier.isSync()).toBeFalsy();
expect(relier.wantsKeys()).toBeFalsy();
expect(relier.isTrusted()).toBeFalsy();
expect(await relier.isTrusted()).toBeFalsy();
});
});

Expand All @@ -268,7 +274,7 @@ describe('lib/reliers/relier-factory', () => {
expect(relier.isOAuth()).toBeTruthy();
expect(await relier.isSync()).toBeFalsy();
expect(relier.wantsKeys()).toBeFalsy();
expect(relier.isTrusted()).toBeFalsy();
expect(await relier.isTrusted()).toBeFalsy();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export class OAuthRedirectIntegration {
const clientKeyData = await this.callbacks.getOAuthScopedKeyData(
sessionToken,
this.relier.clientId,
this.relier.scope
await this.relier.getNormalizedScope()
);

if (clientKeyData && Object.keys(clientKeyData).length > 0) {
Expand Down Expand Up @@ -127,7 +127,7 @@ export class OAuthRedirectIntegration {
acr_values: this.relier.acrValues,
code_challenge: this.relier.codeChallenge,
code_challenge_method: this.relier.codeChallengeMethod,
scope: this.relier.scope,
scope: await this.relier.getNormalizedScope(),
};
if (keysJwe) {
opts.keys_jwe = keysJwe;
Expand Down Expand Up @@ -159,7 +159,7 @@ export class OAuthRedirectIntegration {
code: string;
state: string;
}) {
// Ensure a redirect was provided. With out this info, we can't relay the oauth code
// Ensure a redirect was provided. Without this info, we can't relay the oauth code
// and state!
if (!this.relier.redirectTo) {
throw new OAuthErrorInvalidRedirectUri();
Expand Down
4 changes: 2 additions & 2 deletions packages/fxa-settings/src/models/reliers/base-relier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ describe('BaseRelier Model', function () {
});

describe('isTrusted', function () {
it('returns `true`', function () {
expect(model.isTrusted()).toBeTruthy();
it('returns `true`', async () => {
expect(await model.isTrusted()).toBeTruthy();
});
});

Expand Down
4 changes: 2 additions & 2 deletions packages/fxa-settings/src/models/reliers/base-relier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export interface Relier extends RelierData {
shouldOfferToSync(view: string): boolean;
wantsKeys(): boolean;
wantsTwoStepAuthentication(): boolean;
isTrusted(): boolean;
isTrusted(): Promise<boolean>;
validate(): void;
getService(): string | undefined;
getRedirectUri(): string | undefined;
Expand Down Expand Up @@ -199,7 +199,7 @@ export class BaseRelier extends ModelDataProvider implements Relier {
return this.service;
}

isTrusted() {
async isTrusted() {
return true;
}

Expand Down
169 changes: 168 additions & 1 deletion packages/fxa-settings/src/models/reliers/oauth-relier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import exp from 'constants';
import { ModelDataStore, GenericData } from '../../lib/model-data';
import { OAuthRelier } from './oauth-relier';
import { OAuthRelier, replaceItemInArray } from './oauth-relier';

describe('models/reliers/oauth-relier', function () {
let data: ModelDataStore;
Expand All @@ -25,5 +26,171 @@ describe('models/reliers/oauth-relier', function () {
expect(model).toBeDefined();
});

describe('scope', () => {
const SCOPE = 'profile:email profile:uid';
const SCOPE_PROFILE = 'profile';
const SCOPE_PROFILE_UNRECOGNIZED = 'profile:unrecognized';
const SCOPE_WITH_PLUS = 'profile:email+profile:uid';
const SCOPE_WITH_EXTRAS =
'profile:email profile:uid profile:non_whitelisted';
const SCOPE_WITH_OPENID = 'profile:email profile:uid openid';

function getRelierWithScope(scope: string) {
const relier = new OAuthRelier(
new GenericData({
scope,
}),
new GenericData({}),
{
scopedKeysEnabled: true,
scopedKeysValidation: {},
isPromptNoneEnabled: true,
isPromptNoneEnabledClientIds: [],
}
);

relier.isTrusted = async () => {
return true;
};

return relier;
}

describe('is invalid', () => {
function getRelier(scope: string) {
return getRelierWithScope(scope);
}

it('empty scope', async () => {
const relier = getRelier('');
await expect(relier.getPermissions()).rejects.toThrow();
});

it('whitespace scope', async () => {
const relier = getRelier(' ');
await expect(relier.getPermissions()).rejects.toThrow();
});
});

describe('is valid', () => {
function getRelier(scope: string) {
return getRelierWithScope(scope);
}

it(`normalizes ${SCOPE}`, async () => {
const relier = getRelier(SCOPE);
expect(await relier.getNormalizedScope()).toEqual(
'profile:email profile:uid'
);
});

it(`transforms ${SCOPE} to permissions`, async () => {
const relier = getRelier(SCOPE);
expect(await relier.getPermissions()).toEqual([
'profile:email',
'profile:uid',
]);
});

it(`transforms ${SCOPE_WITH_PLUS}`, async () => {
const relier = getRelier(SCOPE_WITH_PLUS);
expect(await relier.getPermissions()).toEqual([
'profile:email',
'profile:uid',
]);
});
});

describe('untrusted reliers', () => {
function getRelier(scope: string) {
const relier = getRelierWithScope(scope);
relier.isTrusted = async () => {
return false;
};
return relier;
}

it(`normalizes ${SCOPE_WITH_EXTRAS}`, async () => {
const relier = getRelier(SCOPE_WITH_EXTRAS);
expect(await relier.getNormalizedScope()).toBe(SCOPE);
});

it(`normalizes ${SCOPE_WITH_OPENID}`, async () => {
const relier = getRelier(SCOPE_WITH_OPENID);
expect(await relier.getNormalizedScope()).toBe(SCOPE_WITH_OPENID);
});

it(`prohibits ${SCOPE_PROFILE}`, async () => {
const relier = getRelier(SCOPE_PROFILE);
await expect(relier.getNormalizedScope()).rejects.toThrow();
});

it(`prohibits ${SCOPE_PROFILE_UNRECOGNIZED}`, async () => {
const relier = getRelier(SCOPE_PROFILE_UNRECOGNIZED);
await expect(relier.getNormalizedScope()).rejects.toThrow();
});
});

describe('trusted reliers that do not ask for consent', () => {
function getRelier(scope: string) {
const relier = getRelierWithScope(scope);
relier.wantsConsent = () => {
return false;
};
return relier;
}

it(`normalizes ${SCOPE_WITH_EXTRAS}`, async () => {
const relier = getRelier(SCOPE_WITH_EXTRAS);
expect(await relier.getNormalizedScope()).toEqual(SCOPE_WITH_EXTRAS);
});

it(`normalizes ${SCOPE_PROFILE}`, async () => {
const relier = getRelier(SCOPE_PROFILE);
expect(await relier.getNormalizedScope()).toEqual(SCOPE_PROFILE);
});

it(`normalizes ${SCOPE_PROFILE_UNRECOGNIZED}`, async () => {
const relier = getRelier(SCOPE_PROFILE_UNRECOGNIZED);
expect(await relier.getNormalizedScope()).toEqual(
SCOPE_PROFILE_UNRECOGNIZED
);
});
});
});

describe('replaceItemInArray', () => {
it('handles empty array', () => {
expect(replaceItemInArray([], 'foo', ['bar'])).toEqual([]);
});

it('handles miss', () => {
expect(replaceItemInArray(['a', 'b', 'c'], '', ['foo'])).toEqual([
'a',
'b',
'c',
]);
});

it('replaces and preserves order', () => {
expect(replaceItemInArray(['a', 'b', 'c'], 'b', ['foo', 'bar'])).toEqual([
'a',
'foo',
'bar',
'c',
]);
});

it('handles duplicates', () => {
expect(
replaceItemInArray(['a', 'b', 'b', 'c', 'c'], 'b', ['foo', 'foo'])
).toEqual(['a', 'foo', 'c']);
});

it('handles empty replacement', () => {
expect(replaceItemInArray(['a', 'b', 'c'], 'a', [])).toEqual(['b', 'c']);
});
});

// TODO: OAuth Relier Model Test Coverage
});
Loading

0 comments on commit 7034e51

Please sign in to comment.