Skip to content

[LINK-2174] add support for authnMethod config #110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Nov 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ typings/
.env

# next.js build output
.next
.next
20 changes: 19 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,23 @@ mtLinkSdk.init(clientId, options);
| options.mode | `production`, `staging`, `develop`, `local` | false | `production` | <p>Environment for the SDK to connect to, the SDK will connect to the Moneytree production server by default.<ul><li>Moneytree clients should use `staging` for development as `develop` may contain unstable features.</li><li>`local` should only be used for SDK development as it has local dependencies.</li></ul></p> |
| options.locale | string | false | Auto detect. | Force Moneytree to load content in this specific locale. A default value will be auto detected based on guest langauges configurations and location if available. Check this [spec](https://www.w3.org/TR/html401/struct/dirlang.html#h-8.1.1) for more information.<br /><br />Currently supported values are:<br />`en`, `en-AU`, `ja`. |
| options.cobrandClientId (private) | string | false | | <strong>NOTE: This is an internal attribute. Please do not use it unless instructed by your integration representative.</strong><br /><br />Brand Moneytree apps with client's branding. E.g: logo or theme. |
| options.samlSubjectId | string | false | | Sets subject Id for saml session version. |

### setSamlSubjectId

The `setSamlSubjectId` method is used to set the value of the `saml_subject_id` parameter.
The `setSamlSubjectId` parameter allows the client to pass a guest identifier to Moneytree so that Moneytree can forward it to the Identity Provider (IdP) via the SAMLRequest.
The `saml_subject_id` parameter will be forwarded to the `authorize`, `logout` and `open-service` methods when defined.

<h6>Usage:</h6>

```javascript
mtLinkSdk.setSamlSubjectId(samlSubjectId);
```

| Parameter | Type | Required | Default Value | Description |
| ------------- | ------ | -------- | ------------- | ---------------------------------------- |
| sublSubjectId | string | true | | <p>Set the saml_subject_id parameter</p> |

### authorize

Expand Down Expand Up @@ -320,8 +337,9 @@ These common options are used in multiple APIs. Instead of repeating the same op
| options.backTo | string | false | Value set during `init`. | A redirection URL for redirecting a guest back to in the following condition: <li>Guest clicks on `Back to [App Name]` button in any Moneytree screen.</li><li>Guest refuses to give consent to access permission in the consent screen.</li><li>Guest logs out from Moneytree via an app with this client id</li><li>Revoke an app's consent from settings screen opened via an app with this client id</li><br /><br /><strong>NOTE:</strong> No `Back to [App Name]` button will be shown if this value is not set, and any of the actions mentioned above will redirect the guest back to login screen by default. |
| <span id="authorize_option_auth_action">options.authAction</span> | `login`, `signup` | false | Value set during `init`.<p><strong>OR</strong></p>`login` | Show login or sign up screen when a session does not exist during an `authorize` call. |
| options.showAuthToggle | boolean | false | Value set during `init`.<p><strong>OR</strong></p>`true` | If you wish to disable the login to sign up form toggle button and vice-versa in the auth screen, set this to `false`. |
| options.showRememberMe | boolean | false | Value set during `init`.<p><strong>OR</strong></p>`true` | If you wish to disable the `Stay logged in for 30 days` checkbox in the login screen, set this to `false`. |
| options.showRememberMe | boolean | false | Value set during `init`.<p><strong>OR</strong></p>`true` | If you wish to disable the `Stay logged in for 30 days` checkbox in the login screen, set this to `false`. |
| options.isNewTab | boolean | false | Value set during `init`.<p><strong>OR</strong></p>`false` | Call method and open/render in a new browser tab, by default all views open in the same tab. |
| options.authnMethod | string | | Value set during `init`. | Use different authentication methods. This can be a string with the following values:<br /><br />`sso`, `passwordless`, `credentials`. |
| options.sdkPlatform (private) | string | false | Generated by the SDK. | <strong>NOTE: this is for Moneytree internal use, please do not use it to avoid unintended behavior!</strong><br /><br />Indicating sdk platform. |
| options.sdkVersion (private) | semver | false | Generated by the SDK. | <strong>NOTE: this is for Moneytree internal use, please do not use it to avoid unintended behavior!</strong><br /><br />Indicating sdk version. |

Expand Down
74 changes: 59 additions & 15 deletions src/__tests__/helper.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { constructScopes, getIsTabValue, mergeConfigs, generateConfigs } from '../helper';
import packageJson from '../../package.json';
import { AuthAction, AuthnMethod, ConfigsOptions } from '../typings';

describe('helper', () => {
test('constuctScopes', () => {
Expand All @@ -23,16 +24,18 @@ describe('helper', () => {
backTo: 'backTo',
authAction: 'signup',
showAuthToggle: true,
showRememberMe: true
showRememberMe: true,
authnMethod: 'sso'
},
{}
)
).toMatchObject({
).toEqual({
authAction: 'signup',
email: 'email',
backTo: 'backTo',
authAction: 'signup',
showRememberMe: true,
showAuthToggle: true,
showRememberMe: true
authnMethod: 'sso'
});
});

Expand Down Expand Up @@ -73,13 +76,14 @@ describe('helper', () => {
showAuthToggle: true,
showRememberMe: true,
// @ts-ignore: set unsupported key
whatIsThis: false
whatIsThis: false,
authnMethod: 'not really valid' as AuthnMethod
},
{
whatIsThis2: false
}
)
).toMatchObject({
).toEqual({
email: 'email',
backTo: 'backTo',
authAction: 'signup',
Expand Down Expand Up @@ -116,15 +120,55 @@ describe('helper', () => {

describe('generateConfigs', () => {
test('with parameter', () => {
expect(
generateConfigs({
email: 'email',
backTo: 'backTo',
authAction: 'signup',
showAuthToggle: true,
showRememberMe: true
})
).toBe(
const configPayload: ConfigsOptions = {
email: 'email',
backTo: 'backTo',
authAction: 'signup',
showAuthToggle: true,
showRememberMe: true,
authnMethod: 'sso'
};

expect(generateConfigs(configPayload)).toBe(
`sdk_platform=js&sdk_version=${packageJson.version}&email=email&back_to=backTo&auth_action=signup&show_auth_toggle=true` +
`&show_remember_me=true&authn_method=sso`
);
});

test('query encoding should make sure config params are also encoded', () => {
const configPayload: ConfigsOptions = {
email: 'email&!@#(*)-304should be_encoded',
backTo: 'backTo #!@with []special= chars',
authAction: 'signup',
showAuthToggle: true,
showRememberMe: true,
authnMethod: 'sso'
};

expect(generateConfigs(configPayload)).toBe(
`sdk_platform=js&sdk_version=${packageJson.version}&email=email%26%21%40%23%28%2A%29-304should%20be_encoded&back_to=backTo%20%23%21%40with%20%5B%5Dspecial%3D%20chars&auth_action=signup&show_auth_toggle=true&show_remember_me=true&authn_method=sso`
);
});

test('Should raise an error when passing an array in authnMethod', () => {
const configPayload: ConfigsOptions = {
authnMethod: ['oh-not-valid', 'should raise'] as unknown as AuthnMethod
};

expect(() => generateConfigs(configPayload)).toThrow(TypeError);
});

test('Should reject invalid authnMethod from config', () => {
const configPayload: ConfigsOptions = {
email: 'email',
backTo: 'backTo',
authAction: 'signup',
showAuthToggle: true,
showRememberMe: true,
authnMethod: 'oh-not-valid' as AuthnMethod
};

expect(generateConfigs(configPayload)).toBe(
`sdk_platform=js&sdk_version=${packageJson.version}&email=email&back_to=backTo&auth_action=signup&show_auth_toggle=true` +
`&show_remember_me=true`
);
Expand Down
23 changes: 21 additions & 2 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,19 @@ describe('index', () => {
const instance = new MtLinkSdk();

instance.init('clientId', {
redirectUri: 'redirectUri'
redirectUri: 'redirectUri',
authnMethod: 'sso',
samlSubjectId: 'samlSubjectId'
});

const options = instance.storedOptions;
const storedOptions = {
clientId: options.clientId,
mode: options.mode,
redirectUri: options.redirectUri,
state: options.state
state: options.state,
authnMethod: options.authnMethod,
samlSubjectId: options.samlSubjectId
};

const result1 = instance.authorize({ scopes: 'scopes' });
Expand Down Expand Up @@ -94,4 +98,19 @@ describe('index', () => {

expect(mtLinkSdk.storedOptions.mode).toBe('production');
});

test('sets the samlSubjectId when calling setSamlSubjectId', () => {
const samlSubjectId = 'samlSubjectId';
mtLinkSdk.init('clientId', {
samlSubjectId
});

expect(mtLinkSdk.storedOptions.samlSubjectId).toBe(samlSubjectId);

const newSamlSubjectId = 'newSamlSubjectId';

mtLinkSdk.setSamlSubjectId(newSamlSubjectId);

expect(mtLinkSdk.storedOptions.samlSubjectId).toBe(newSamlSubjectId);
});
});
9 changes: 7 additions & 2 deletions src/api/__tests__/authorize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@ describe('api', () => {
const scopes = 'points_read';
const cobrandClientId = 'cobrandClientId';
const locale = 'locale';
const samlSubjectId = 'mySubject';

const mtLinkSdk = new MtLinkSdk();
mtLinkSdk.init(clientId, {
redirectUri,
scopes,
locale,
cobrandClientId
cobrandClientId,
samlSubjectId
});

authorize(mtLinkSdk.storedOptions);
Expand All @@ -64,6 +66,7 @@ describe('api', () => {
redirect_uri: redirectUri,
country,
locale,
saml_subject_id: samlSubjectId,
configs: generateConfigs()
});
const url = `${MY_ACCOUNT_DOMAINS.production}/oauth/authorize?${query}`;
Expand All @@ -77,9 +80,10 @@ describe('api', () => {
const state = 'state';
const country = 'JP';
const scopes = 'points_read';
const samlSubjectId = 'mySubject';

const mtLinkSdk = new MtLinkSdk();
mtLinkSdk.init(clientId);
mtLinkSdk.init(clientId, { samlSubjectId });

authorize(mtLinkSdk.storedOptions, {
state,
Expand All @@ -96,6 +100,7 @@ describe('api', () => {
redirect_uri: redirectUri,
state,
country,
saml_subject_id: samlSubjectId,
configs: generateConfigs()
});
const url = `${MY_ACCOUNT_DOMAINS.production}/oauth/authorize?${query}`;
Expand Down
39 changes: 39 additions & 0 deletions src/api/__tests__/open-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,45 @@ describe('api', () => {
}).toThrow('[mt-link-sdk] Invalid `serviceId` in `openService`, got: invalid');
});

test('saml_subject_id is passed when initialized', () => {
open.mockClear();

const instance = new MtLinkSdk();
instance.init('clientId', { samlSubjectId: 'samlSubjectId' });

openService(instance.storedOptions, 'myaccount');

expect(open).toBeCalledTimes(1);

const query = qs.stringify({
client_id: 'clientId',
saml_subject_id: 'samlSubjectId',
configs: generateConfigs()
});
const url = `${MY_ACCOUNT_DOMAINS.production}/?${query}`;

expect(open).toBeCalledWith(url, '_self', 'noreferrer');
});

test('undefined saml_subject_id should not be passed down', () => {
open.mockClear();

const instance = new MtLinkSdk();
instance.init('clientId', { samlSubjectId: undefined });

openService(instance.storedOptions, 'myaccount');

expect(open).toBeCalledTimes(1);

const query = qs.stringify({
client_id: 'clientId',
configs: generateConfigs()
});
const url = `${MY_ACCOUNT_DOMAINS.production}/?${query}`;

expect(open).toBeCalledWith(url, '_self', 'noreferrer');
});

test('without window', () => {
const windowSpy = jest.spyOn(global, 'window', 'get');
// @ts-ignore: mocking window object to undefined
Expand Down
4 changes: 3 additions & 1 deletion src/api/authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export default function authorize(storedOptions: StoredOptions, options: Authori
cobrandClientId,
locale,
scopes: defaultScopes,
redirectUri: defaultRedirectUri
redirectUri: defaultRedirectUri,
samlSubjectId
} = storedOptions;

if (!clientId) {
Expand Down Expand Up @@ -61,6 +62,7 @@ export default function authorize(storedOptions: StoredOptions, options: Authori
state,
country: 'JP',
locale,
saml_subject_id: samlSubjectId,
configs: generateConfigs(mergeConfigs(storedOptions, rest))
});

Expand Down
3 changes: 2 additions & 1 deletion src/api/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ export default function logout(storedOptions: StoredOptions, options: LogoutOpti
throw new Error(`[mt-link-sdk] \`logout\` only works in the browser.`);
}

const { clientId, mode, cobrandClientId, locale } = storedOptions;
const { clientId, mode, cobrandClientId, locale, samlSubjectId } = storedOptions;
const { isNewTab, ...rest } = options;

const queryString = stringify({
client_id: clientId,
cobrand_client_id: cobrandClientId,
locale,
saml_subject_id: samlSubjectId,
configs: generateConfigs(mergeConfigs(storedOptions, rest))
});

Expand Down
4 changes: 3 additions & 1 deletion src/api/open-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface QueryData {
cobrand_client_id?: string;
locale?: string;
configs: string;
saml_subject_id?: string;
}

export default function openService(
Expand All @@ -27,14 +28,15 @@ export default function openService(
throw new Error('[mt-link-sdk] `openService` only works in the browser.');
}

const { clientId, mode, cobrandClientId, locale } = storedOptions;
const { clientId, mode, cobrandClientId, locale, samlSubjectId } = storedOptions;
const { isNewTab, view = '', ...rest } = options;

const getQueryValue = (needStringify = true): string | QueryData => {
const query: QueryData = {
client_id: clientId,
cobrand_client_id: cobrandClientId,
locale,
saml_subject_id: samlSubjectId,
configs: generateConfigs(mergeConfigs(storedOptions, rest))
};

Expand Down
Loading