diff --git a/.gitignore b/.gitignore
index 42ea526..a89c861 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,4 +61,4 @@ typings/
.env
# next.js build output
-.next
\ No newline at end of file
+.next
diff --git a/docs/README.md b/docs/README.md
index 1f73677..dfa4b3f 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -65,6 +65,23 @@ mtLinkSdk.init(clientId, options);
| options.mode | `production`, `staging`, `develop`, `local` | false | `production` |
Environment for the SDK to connect to, the SDK will connect to the Moneytree production server by default.
- Moneytree clients should use `staging` for development as `develop` may contain unstable features.
- `local` should only be used for SDK development as it has local dependencies.
|
| 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.
Currently supported values are:
`en`, `en-AU`, `ja`. |
| options.cobrandClientId (private) | string | false | | NOTE: This is an internal attribute. Please do not use it unless instructed by your integration representative.
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.
+
+Usage:
+
+```javascript
+mtLinkSdk.setSamlSubjectId(samlSubjectId);
+```
+
+| Parameter | Type | Required | Default Value | Description |
+| ------------- | ------ | -------- | ------------- | ---------------------------------------- |
+| sublSubjectId | string | true | | Set the saml_subject_id parameter
|
### authorize
@@ -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: Guest clicks on `Back to [App Name]` button in any Moneytree screen.Guest refuses to give consent to access permission in the consent screen.Guest logs out from Moneytree via an app with this client idRevoke an app's consent from settings screen opened via an app with this client id
NOTE: 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. |
| options.authAction | `login`, `signup` | false | Value set during `init`.OR
`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`.OR
`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`.OR
`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`.OR
`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`.OR
`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:
`sso`, `passwordless`, `credentials`. |
| options.sdkPlatform (private) | string | false | Generated by the SDK. | NOTE: this is for Moneytree internal use, please do not use it to avoid unintended behavior!
Indicating sdk platform. |
| options.sdkVersion (private) | semver | false | Generated by the SDK. | NOTE: this is for Moneytree internal use, please do not use it to avoid unintended behavior!
Indicating sdk version. |
diff --git a/src/__tests__/helper.test.ts b/src/__tests__/helper.test.ts
index c652523..89f41b2 100644
--- a/src/__tests__/helper.test.ts
+++ b/src/__tests__/helper.test.ts
@@ -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', () => {
@@ -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'
});
});
@@ -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',
@@ -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`
);
diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts
index 3c87635..94d809c 100644
--- a/src/__tests__/index.test.ts
+++ b/src/__tests__/index.test.ts
@@ -22,7 +22,9 @@ describe('index', () => {
const instance = new MtLinkSdk();
instance.init('clientId', {
- redirectUri: 'redirectUri'
+ redirectUri: 'redirectUri',
+ authnMethod: 'sso',
+ samlSubjectId: 'samlSubjectId'
});
const options = instance.storedOptions;
@@ -30,7 +32,9 @@ describe('index', () => {
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' });
@@ -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);
+ });
});
diff --git a/src/api/__tests__/authorize.test.ts b/src/api/__tests__/authorize.test.ts
index 4c6b2f9..3540e30 100644
--- a/src/api/__tests__/authorize.test.ts
+++ b/src/api/__tests__/authorize.test.ts
@@ -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);
@@ -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}`;
@@ -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,
@@ -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}`;
diff --git a/src/api/__tests__/open-service.test.ts b/src/api/__tests__/open-service.test.ts
index e7bcee6..2bac17e 100644
--- a/src/api/__tests__/open-service.test.ts
+++ b/src/api/__tests__/open-service.test.ts
@@ -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
diff --git a/src/api/authorize.ts b/src/api/authorize.ts
index a9a8198..5611dff 100644
--- a/src/api/authorize.ts
+++ b/src/api/authorize.ts
@@ -23,7 +23,8 @@ export default function authorize(storedOptions: StoredOptions, options: Authori
cobrandClientId,
locale,
scopes: defaultScopes,
- redirectUri: defaultRedirectUri
+ redirectUri: defaultRedirectUri,
+ samlSubjectId
} = storedOptions;
if (!clientId) {
@@ -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))
});
diff --git a/src/api/logout.ts b/src/api/logout.ts
index 3ee3348..249b535 100644
--- a/src/api/logout.ts
+++ b/src/api/logout.ts
@@ -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))
});
diff --git a/src/api/open-service.ts b/src/api/open-service.ts
index d69b039..99c1a66 100644
--- a/src/api/open-service.ts
+++ b/src/api/open-service.ts
@@ -16,6 +16,7 @@ interface QueryData {
cobrand_client_id?: string;
locale?: string;
configs: string;
+ saml_subject_id?: string;
}
export default function openService(
@@ -27,7 +28,7 @@ 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 => {
@@ -35,6 +36,7 @@ export default function openService(
client_id: clientId,
cobrand_client_id: cobrandClientId,
locale,
+ saml_subject_id: samlSubjectId,
configs: generateConfigs(mergeConfigs(storedOptions, rest))
};
diff --git a/src/helper.ts b/src/helper.ts
index 62b89b9..d0fd8e5 100644
--- a/src/helper.ts
+++ b/src/helper.ts
@@ -7,7 +7,17 @@ import { encode } from 'url-safe-base64';
import { v4 as uuid } from 'uuid';
import storage from './storage';
-import { Scopes, InitOptions, ConfigsOptions, AuthAction } from './typings';
+import {
+ Scopes,
+ InitOptions,
+ ConfigsOptions,
+ AuthAction,
+ AuthnMethod,
+ supportedAuthnMethod,
+ supportedAuthAction,
+ supportedConfigsOptions,
+ SupportedConfigsOptions
+} from './typings';
export function constructScopes(scopes: Scopes = ''): string | undefined {
return (Array.isArray(scopes) ? scopes.join(' ') : scopes) || undefined;
@@ -27,7 +37,8 @@ export function mergeConfigs(
backTo: defaultBackTo,
authAction: defaultAuthAction,
showAuthToggle: defaultShowAuthToggle,
- showRememberMe: defaultShowRememberMe
+ showRememberMe: defaultShowRememberMe,
+ authnMethod: defaultAuthnMethod
} = initValues;
const {
@@ -36,27 +47,29 @@ export function mergeConfigs(
authAction = defaultAuthAction,
showAuthToggle = defaultShowAuthToggle,
showRememberMe = defaultShowRememberMe,
+ authnMethod: rawAuthnMethod = defaultAuthnMethod,
...rest
} = newValues;
+ const authnMethod = parseAuthnMethod(rawAuthnMethod);
+
const configs: ConfigsOptions = {
...rest,
email,
backTo,
authAction,
showAuthToggle,
- showRememberMe
+ showRememberMe,
+ authnMethod
};
- if (ignoreKeys.length) {
- const keys = Object.keys(configs) as Array;
+ const keys = Object.keys(configs) as Array;
- for (let i = 0; i < keys.length; i++) {
- const key = keys[i];
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
- if (ignoreKeys.indexOf(key) !== -1) {
- configs[key] = undefined;
- }
+ if (ignoreKeys.indexOf(key) !== -1 || !supportedConfigsOptions.includes(key)) {
+ configs[key] = undefined;
}
}
@@ -75,9 +88,18 @@ export function generateConfigs(configs: ConfigsOptions = {}): string {
'isNewTab',
'forceLogout',
'sdkPlatform',
- 'sdkVersion'
+ 'sdkVersion',
+ 'authnMethod'
];
+ if (configs.authnMethod) {
+ configs.authnMethod = parseAuthnMethod(configs.authnMethod);
+ }
+
+ if (configs.authAction) {
+ configs.authAction = parseAuthAction(configs.authAction);
+ }
+
for (const key in configs) {
if (configKeys.indexOf(key) !== -1) {
snakeCaseConfigs[snakeCase(key)] = configs[key as keyof ConfigsOptions];
@@ -91,6 +113,26 @@ export function generateConfigs(configs: ConfigsOptions = {}): string {
});
}
+function isAuthnMethod(x: unknown): x is AuthnMethod {
+ return supportedAuthnMethod.includes(x as AuthnMethod);
+}
+
+function parseAuthnMethod(x: unknown): AuthnMethod | undefined {
+ if (Array.isArray(x)) {
+ throw new TypeError('Array is not allowed for authnMethod');
+ }
+
+ return isAuthnMethod(x) ? x : undefined;
+}
+
+function isAuthAction(x: unknown): x is AuthAction {
+ return supportedAuthAction.includes(x as AuthAction);
+}
+
+function parseAuthAction(x: unknown): AuthAction | undefined {
+ return isAuthAction(x) ? x : undefined;
+}
+
export function generateCodeChallenge(): string {
const codeVerifier = uuid();
diff --git a/src/index.ts b/src/index.ts
index e277b78..9feac49 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -44,6 +44,10 @@ export class MtLinkSdk {
};
}
+ public setSamlSubjectId(value: string): void {
+ this.storedOptions.samlSubjectId = value;
+ }
+
public authorize(options?: AuthorizeOptions): void {
authorize(this.storedOptions, options);
}
diff --git a/src/typings.ts b/src/typings.ts
index 39a2aba..1805fff 100644
--- a/src/typings.ts
+++ b/src/typings.ts
@@ -1,7 +1,9 @@
-export type AuthAction = 'login' | 'signup';
+export const supportedAuthAction = ['login', 'signup'] as const;
+export type AuthAction = typeof supportedAuthAction[number];
export interface PrivateParams {
cobrandClientId?: string;
+ samlSubjectId?: string;
}
export interface PrivateConfigsOptions {
@@ -9,6 +11,20 @@ export interface PrivateConfigsOptions {
sdkVersion?: string; // semver
}
+export const supportedAuthnMethod = ['passwordless', 'sso', 'credentials'] as const;
+export type AuthnMethod = typeof supportedAuthnMethod[number];
+
+export const supportedConfigsOptions = [
+ 'email',
+ 'backTo',
+ 'authAction',
+ 'showAuthToggle',
+ 'showRememberMe',
+ 'isNewTab',
+ 'forceLogout',
+ 'authnMethod'
+] as const;
+export type SupportedConfigsOptions = typeof supportedConfigsOptions[number];
export interface ConfigsOptions extends PrivateConfigsOptions {
email?: string;
backTo?: string;
@@ -17,6 +33,7 @@ export interface ConfigsOptions extends PrivateConfigsOptions {
showRememberMe?: boolean;
isNewTab?: boolean;
forceLogout?: boolean;
+ authnMethod?: AuthnMethod;
}
export type ServicesListType = {
@@ -76,11 +93,11 @@ export type InitOptions = Omit, 'code
mode?: Mode;
locale?: string;
};
+
export interface StoredOptions extends InitOptions {
clientId?: string;
mode: Mode;
}
-
export interface ExchangeTokenOptions extends OAuthSharedParams {
code?: string;
codeVerifier?: string;