Skip to content

Commit bc6bfe5

Browse files
committed
feat: authorize & onboard accepts pkce & codeChallenge options
`codeChallenge` - usually generated from a server from a code verifier, pass this value to the authorize or onboard call and it redirects back to the server where the token exchanging happens without exposing the code verifier to the client side application. `pkce` - this will tells the SDK to auto generate a code challenge, this use case is for an application who wish to authorize or onboard with PKCE flow and exchange the token on the client side.
1 parent c5f2bda commit bc6bfe5

File tree

7 files changed

+61
-54
lines changed

7 files changed

+61
-54
lines changed

docs/README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,9 @@ mtLinkSdk.authorize(options);
8484
| <span id="api-authorize_options">options</span> | object | false | Value set during `init`. | Optional parameters as described in [common options](#common-api-options). |
8585
| options.scopes | string <p><strong>OR</strong></p> string[] | false | Value set during `init`.<p><strong>OR</strong></p>`guest_read` | Access scopes you're requesting. This can be a single scope, or an array of scopes.<br /><br />Currently supported scopes are:<br />`guest_read`, `accounts_read`, `points_read`, `point_transactions_read`, `transactions_read`, `transactions_write`, `expense_claims_read`, `categories_read`, `investment_accounts_read`, `investment_transactions_read`, `notifications_read`, `request_refresh`, `life_insurance_read`. |
8686
| options.redirectUri | string | false | Value set during `init`. | OAuth redirection URI, refer [here](https://www.oauth.com/oauth2-servers/redirect-uris/) for more details.<br /><br /><strong>NOTE:</strong> This function will throw an error if this value is undefined <strong>and</strong> no default value was provided in the [init options](?id=api-init_options). |
87-
| options.state | string | false | You can pass in an optional value here during OAuth authorization request and validate the value is still same after an OAuth redirection. [Click here](https://tools.ietf.org/html/rfc6749#section-4.1.1)|
88-
| options.codeVerifier | string | false | Value set during `init`. | We only support SHA256, therefore this `codeVerifier` will be used to generate the `code_challenge` using the SHA256 hash algorithm. [Click here](https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce) for more details.</p><strong>NOTE:</strong> Make sure to set this value explicitly if your server generates an identifier for the OAuth authorization request so that you can use to acquire the access token after the OAuth redirect occurs. |
87+
| options.state | string | false | Value set during `init`.<p><strong>OR</strong></p>Randomly generated [uuid](<https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)>). | Refer [here](https://auth0.com/docs/protocols/oauth2/oauth-state) for more details.<br /><br /><strong>NOTE:</strong> Make sure to set this value explicitly if your server generates an identifier for the OAuth authorization request so that you can use to acquire the access token after the OAuth redirect occurs. |
88+
| options.codeChallenge | string | false | | We only support SHA256 as code challenge method, therefore please ensure the `code_challenge` was generated using the SHA256 hash algorithm. [Click here](https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce) for more details.</p><strong>NOTE:</strong> Set this value only if your server wish to use PKCE flow. |
89+
| options.pkce | boolean | false | false | Set to `true` if you wish to use PKCE flow on the client side, SDK will automatically generate the code challenge from a locally generated code verifier and use the code verifier in [exchangeToken](#exchangetoken). |
8990
| <span id="authorize_option_force_logout">options.forceLogout</span> | boolean | false | `false` | Force existing guest session to logout and call authorize with a clean state. |
9091
| options.country | `AU`, ` JP` | false | Value set during `init`. | Server location for the guest to login or sign up. If you wish to restrict your guest to only one country, make sure to set this value.<br /><br /><strong>NOTE:</strong> For apps created after 2020-07-08, the sign up form will display a country selection dropdown for the guest to select a country when this value is undefined or invalid. |
9192

@@ -117,8 +118,6 @@ mtLinkSdk.onboard(options)
117118

118119
Since we are using PKCE/Code grant, we will have to exchange the `code` for a token. You can optionally pass `code` via options parameter or it will fallback to automatically extract it from the browser URL.
119120

120-
Options for the, `codeVerifier` and `onboard` calls will use the default value from `init`, however if you explicitly pass a new value when calling `authorize` or `onboard` via the options parameter, make sure to reuse the same value when calling this API, otherwise the authentication server will throw an error due to a value mismatch.
121-
122121
`code` will be invalidated (can be used only once) after exchanged for a token, it is your responsibility to store the token yourself as the SDK does not store it internally.
123122

124123
Refer [here](https://www.oauth.com/oauth2-servers/pkce/authorization-code-exchange/) for more details.
@@ -138,7 +137,7 @@ const token = await mtLinkSdk.exchangeToken(options);
138137
| - | - | - | - | - |
139138
| options | object | false | Value set during `init`. | Optional parameters. |
140139
| options.code | string | false | Value from browser URL | Code from OAuth redirection used to exchange for a token, SDK will try to extract it from the browser URL if none is provided.<br /><br /><strong>NOTE:</strong> SDK will throw an error if no value is provided here and the client library failed to extract it from browser URL. |
141-
| options.codeVerifier | string | false | Value set during `init`. | Make sure the value of `codeVerifier` here is the same codeVerifier value used during the `authorize` or `onboard` call. |
140+
| options.codeVerifier | string | false | | If you pass a `codeChallenge` option during the `authorize` or `onboard` call and wish to exchange the token on the client side application, make sure to set the code verifier used to generate the said `codeChallenge` here. |
142141
| options.redirectUri | string | false | Value set during `init`. | Make sure the value of `redirectUri` here is the same redirectUri value used during the `authorize` or `onboard` call.<br /><br /><strong>NOTE:</strong> The SDK will throw an error if both this parameter and the default value from the [init options](?id=api-init_options) are undefined. |
143142

144143
### tokenInfo

src/api/authorize.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { stringify } from 'qs';
2-
import { createHash } from 'crypto';
3-
import { encode } from 'url-safe-base64';
42

5-
import { constructScopes, generateConfigs, mergeConfigs, getIsTabValue } from '../helper';
3+
import {
4+
constructScopes,
5+
generateConfigs,
6+
mergeConfigs,
7+
getIsTabValue,
8+
generateCodeChallenge,
9+
} from '../helper';
610
import { MY_ACCOUNT_DOMAINS } from '../server-paths';
711
import { StoredOptions, AuthorizeOptions } from '../typings';
812
import storage from '../storage';
913

10-
export default function authorize(
11-
storedOptions: StoredOptions,
12-
options: AuthorizeOptions = {}
13-
): void {
14+
export default function authorize(storedOptions: StoredOptions, options: AuthorizeOptions = {}): void {
1415
if (!window) {
1516
throw new Error('[mt-link-sdk] `authorize` only works in the browser.');
1617
}
@@ -22,7 +23,6 @@ export default function authorize(
2223
locale,
2324
scopes: defaultScopes,
2425
redirectUri: defaultRedirectUri,
25-
codeVerifier: defaultCodeVerifier,
2626
country: defaultCountry,
2727
} = storedOptions;
2828

@@ -33,36 +33,32 @@ export default function authorize(
3333
const {
3434
scopes = defaultScopes,
3535
redirectUri = defaultRedirectUri,
36-
codeVerifier = defaultCodeVerifier,
3736
country = defaultCountry,
37+
pkce = false,
38+
codeChallenge,
3839
isNewTab,
3940
state,
4041
...rest
4142
} = options;
4243

43-
// update codeVerifier
44-
if (codeVerifier !== defaultCodeVerifier) {
45-
storage.set('codeVerifier', codeVerifier);
46-
}
47-
4844
if (!redirectUri) {
4945
throw new Error(
5046
'[mt-link-sdk] Missing option `redirectUri` in `authorize`, make sure to pass one via `authorize` options or `init` options.'
5147
);
5248
}
5349

54-
const codeChallenge =
55-
codeVerifier &&
56-
encode(createHash('sha256').update(codeVerifier).digest('base64').split('=')[0]);
50+
storage.del('cv');
51+
52+
const cc = codeChallenge || (pkce && generateCodeChallenge());
5753

5854
const queryString = stringify({
5955
client_id: clientId,
6056
cobrand_client_id: cobrandClientId,
6157
response_type: 'code',
6258
scope: constructScopes(scopes),
6359
redirect_uri: redirectUri,
64-
code_challenge: codeChallenge || undefined,
65-
code_challenge_method: codeVerifier ? 'S256' : undefined,
60+
code_challenge: cc || undefined,
61+
code_challenge_method: cc ? 'S256' : undefined,
6662
state,
6763
country,
6864
locale,

src/api/exchange-token.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import qs from 'qs';
22

33
import { MY_ACCOUNT_DOMAINS } from '../server-paths';
44
import { StoredOptions, ExchangeTokenOptions } from '../typings';
5+
import storage from '../storage';
56

67
function getCode(): string | undefined {
78
// not available in node environment
@@ -23,8 +24,7 @@ export default async function exchangeToken(
2324
const {
2425
clientId,
2526
redirectUri: defaultRedirectUri,
26-
mode,
27-
codeVerifier: defaultCodeVerifier,
27+
mode
2828
} = storedOptions;
2929

3030
if (!clientId) {
@@ -33,8 +33,8 @@ export default async function exchangeToken(
3333

3434
const {
3535
redirectUri = defaultRedirectUri,
36-
codeVerifier = defaultCodeVerifier,
3736
code = getCode(),
37+
codeVerifier,
3838
} = options;
3939

4040
if (!code) {
@@ -60,8 +60,7 @@ export default async function exchangeToken(
6060
client_id: clientId,
6161
grant_type: 'authorization_code',
6262
redirect_uri: redirectUri,
63-
code_verifier: codeVerifier || undefined,
64-
code_challenge_method: codeVerifier ? 'S256' : undefined,
63+
code_verifier: codeVerifier || (code ? storage.get('cv') : undefined),
6564
}),
6665
});
6766

src/api/onboard.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { stringify } from 'qs';
2-
import { createHash } from 'crypto';
3-
import { encode } from 'url-safe-base64';
42

5-
import { constructScopes, generateConfigs, mergeConfigs, getIsTabValue } from '../helper';
3+
import {
4+
constructScopes,
5+
generateConfigs,
6+
mergeConfigs,
7+
getIsTabValue,
8+
generateCodeChallenge,
9+
} from '../helper';
610
import { MY_ACCOUNT_DOMAINS } from '../server-paths';
711
import { StoredOptions, OnboardOptions } from '../typings';
812
import storage from '../storage';
@@ -19,7 +23,6 @@ export default function onboard(storedOptions: StoredOptions, options: OnboardOp
1923
locale,
2024
scopes: defaultScopes,
2125
redirectUri: defaultRedirectUri,
22-
codeVerifier: defaultCodeVerifier,
2326
country: defaultCountry,
2427
} = storedOptions;
2528

@@ -30,32 +33,29 @@ export default function onboard(storedOptions: StoredOptions, options: OnboardOp
3033
const {
3134
scopes = defaultScopes,
3235
redirectUri = defaultRedirectUri,
33-
codeVerifier = defaultCodeVerifier,
3436
country = defaultCountry,
37+
pkce = false,
38+
codeChallenge,
3539
isNewTab,
3640
state,
3741
...rest
3842
} = options;
43+
3944
const configs = mergeConfigs(storedOptions, rest, [
4045
'authAction',
4146
'showAuthToggle',
4247
'showRememberMe',
4348
'forceLogout',
4449
]);
4550

46-
const { email } = configs;
47-
48-
// update codeVerifier
49-
if (codeVerifier !== defaultCodeVerifier) {
50-
storage.set('codeVerifier', codeVerifier);
51-
}
52-
5351
if (!redirectUri) {
5452
throw new Error(
5553
'[mt-link-sdk] Missing option `redirectUri` in `onboard`, make sure to pass one via `onboard` options or `init` options.'
5654
);
5755
}
5856

57+
const { email } = configs;
58+
5959
if (!email) {
6060
throw new Error(
6161
'[mt-link-sdk] Missing option `email` in `onboard`, make sure to pass one via `onboard` options or `init` options.'
@@ -68,18 +68,18 @@ export default function onboard(storedOptions: StoredOptions, options: OnboardOp
6868
);
6969
}
7070

71-
const codeChallenge =
72-
codeVerifier &&
73-
encode(createHash('sha256').update(codeVerifier).digest('base64').split('=')[0]);
71+
storage.del('cv');
72+
73+
const cc = codeChallenge || (pkce && generateCodeChallenge());
7474

7575
const queryString = stringify({
7676
client_id: clientId,
7777
cobrand_client_id: cobrandClientId,
7878
response_type: 'code',
7979
scope: constructScopes(scopes),
8080
redirect_uri: redirectUri,
81-
code_challenge: codeChallenge || undefined,
82-
code_challenge_method: codeVerifier ? 'S256' : undefined,
81+
code_challenge: cc || undefined,
82+
code_challenge_method: cc ? 'S256' : undefined,
8383
state,
8484
country,
8585
locale,

src/helper.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ declare const __VERSION__: string;
22

33
import { stringify } from 'qs';
44
import { snakeCase } from 'snake-case';
5+
import { createHash } from 'crypto';
6+
import { encode } from 'url-safe-base64';
7+
import { v4 as uuid } from 'uuid';
8+
import storage from './storage';
59

610
import { Scopes, InitOptions, ConfigsOptions, AuthAction } from './typings';
711

@@ -83,3 +87,11 @@ export function generateConfigs(configs: ConfigsOptions = {}): string {
8387
...snakeCaseConfigs,
8488
});
8589
}
90+
91+
export function generateCodeChallenge(): string {
92+
const codeVerifier = uuid();
93+
94+
storage.set('cv', codeVerifier);
95+
96+
return encode(createHash('sha256').update(codeVerifier).digest('base64').split('=')[0]);
97+
}

src/index.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ import {
1212
LogoutOptions,
1313
InitOptions,
1414
AuthorizeOptions,
15+
OnboardOptions,
1516
ExchangeTokenOptions,
1617
RequestMagicLinkOptions,
1718
TokenInfo,
1819
Mode,
1920
} from './typings';
20-
import storage from './storage';
2121

2222
export * from './typings';
2323

@@ -26,7 +26,6 @@ const validModes: Mode[] = ['production', 'staging', 'develop', 'local'];
2626
export class MtLinkSdk {
2727
public storedOptions: StoredOptions = {
2828
mode: 'production',
29-
codeVerifier: storage.get('codeVerifier') || '',
3029
};
3130

3231
public init(clientId: string, options: InitOptions = {}): void {
@@ -43,15 +42,13 @@ export class MtLinkSdk {
4342
clientId,
4443
mode: validModes.indexOf(mode) === -1 ? 'production' : mode,
4544
};
46-
47-
storage.set('codeVerifier', this.storedOptions.codeVerifier);
4845
}
4946

5047
public authorize(options?: AuthorizeOptions): void {
5148
authorize(this.storedOptions, options);
5249
}
5350

54-
public onboard(options?: AuthorizeOptions): void {
51+
public onboard(options?: OnboardOptions): void {
5552
onboard(this.storedOptions, options);
5653
}
5754

src/typings.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ interface AuthorizeConfigsOptions {
5959
interface OAuthSharedParams {
6060
state?: string;
6161
redirectUri?: string;
62-
codeVerifier?: string;
6362
}
6463

6564
export interface AuthorizeOptions
@@ -68,22 +67,27 @@ export interface AuthorizeOptions
6867
AuthorizeConfigsOptions {
6968
country?: string;
7069
scopes?: Scopes;
70+
codeChallenge?: string;
71+
pkce?: boolean;
7172
}
7273

7374
export type Mode = 'production' | 'staging' | 'develop' | 'local';
74-
export type InitOptions = Omit<AuthorizeOptions, 'forceLogout'> &
75+
export type InitOptions = Omit<
76+
Omit<Omit<AuthorizeOptions, 'forceLogout'>, 'codeChallenge'>,
77+
'pkce'
78+
> &
7579
PrivateParams & {
7680
mode?: Mode;
7781
locale?: string;
7882
};
7983
export interface StoredOptions extends InitOptions {
8084
clientId?: string;
8185
mode: Mode;
82-
codeVerifier: string;
8386
}
8487

8588
export interface ExchangeTokenOptions extends OAuthSharedParams {
8689
code?: string;
90+
codeVerifier?: string;
8791
}
8892

8993
export type LogoutOptions = ConfigsOptions;

0 commit comments

Comments
 (0)