mirror of
https://github.com/hanzoai/iam-js-sdk.git
synced 2026-10-30 18:34:08 +00:00
The OAuth 2.0 "state" parameter is the canonical defence against CSRF
on the authorization endpoint (RFC 6749 §10.12). It must be
unguessable to an attacker.
Previously `getOrSaveState` used `Math.random().toString(36).slice(2)`,
which:
- is not cryptographically secure (predictable PRNG)
- produces ~10 chars of base36 (~52 bits of effective entropy at best,
far less in practice because Math.random is not uniform)
This switches state generation to a Web Crypto `getRandomValues`-based
helper with a Node `crypto.randomFillSync` fallback for SSR/test
environments where `window.crypto` is not polyfilled (older jsdom).
The output is 32 random bytes encoded as URL-safe base64 (RFC 4648 §5)
without padding — 43 chars, 256 bits of entropy.
Adds two tests covering:
- state has the expected length and only URL-safe base64 chars
- states are unique across many instances (collision probability ~2^-244)
No public API change. `getOrSaveState` keeps the same signature.
125 lines
8.5 KiB
TypeScript
125 lines
8.5 KiB
TypeScript
// Copyright 2021 The IAM Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import Sdk from '../src';
|
|
|
|
const sdkConfig = {
|
|
serverUrl: 'https://iam.hanzo.ai',
|
|
clientId: 'ce89cf9741eccb80367f',
|
|
appName: 'cloud',
|
|
organizationName: 'built-in',
|
|
redirectPath: '/callback',
|
|
};
|
|
|
|
describe('sdk constructor', () => {
|
|
it('with full configs', () => {
|
|
const sdk = new Sdk(sdkConfig);
|
|
|
|
const instanceConfig = sdk['config'];
|
|
expect(instanceConfig.serverUrl).toEqual(sdkConfig.serverUrl);
|
|
expect(instanceConfig.clientId).toEqual(sdkConfig.clientId);
|
|
expect(instanceConfig.appName).toEqual(sdkConfig.appName);
|
|
expect(instanceConfig.organizationName).toEqual(sdkConfig.organizationName);
|
|
expect(instanceConfig.redirectPath).toEqual(sdkConfig.redirectPath);
|
|
});
|
|
|
|
it('config withou redirectPath', () => {
|
|
let config = {
|
|
...sdkConfig,
|
|
redirectPath: undefined,
|
|
};
|
|
const sdk = new Sdk(sdkConfig);
|
|
|
|
const instanceConfig = sdk['config'];
|
|
expect(instanceConfig.redirectPath).toEqual('/callback');
|
|
});
|
|
});
|
|
|
|
describe('getSigninUrl', () => {
|
|
beforeEach(() => {
|
|
sessionStorage.clear();
|
|
});
|
|
|
|
it('redirectPath with relative path', () => {
|
|
const sdk = new Sdk(sdkConfig);
|
|
|
|
expect(sdk.getSigninUrl()).toContain(
|
|
`redirect_uri=${encodeURIComponent(
|
|
window.location.origin + sdkConfig.redirectPath
|
|
)}`
|
|
);
|
|
});
|
|
|
|
it('redirectPath with fully path', () => {
|
|
const config = {
|
|
...sdkConfig,
|
|
redirectPath: 'http://localhost:6001/other-callback',
|
|
};
|
|
const sdk = new Sdk(config);
|
|
|
|
expect(sdk.getSigninUrl()).toContain(
|
|
`redirect_uri=${encodeURIComponent(config.redirectPath)}`
|
|
);
|
|
});
|
|
|
|
it('with fixed state', () => {
|
|
const state = 'test-state';
|
|
const sdk = new Sdk(sdkConfig);
|
|
sessionStorage.setItem('iam-state', state);
|
|
expect(sdk.getSigninUrl()).toContain(`state=${state}`);
|
|
});
|
|
|
|
it('with random state', () => {
|
|
const sdk = new Sdk(sdkConfig);
|
|
const url = sdk.getSigninUrl();
|
|
const state = sessionStorage.getItem('iam-state');
|
|
expect(url).toContain(`state=${state}`);
|
|
});
|
|
|
|
it('generated state is cryptographically strong and URL-safe', () => {
|
|
const sdk = new Sdk(sdkConfig);
|
|
const state = sdk.getOrSaveState();
|
|
// 32 random bytes encode to 43 chars of unpadded URL-safe base64.
|
|
expect(state.length).toBeGreaterThanOrEqual(43);
|
|
// Must contain only URL-safe base64 alphabet (no +, /, =).
|
|
expect(state).toMatch(/^[A-Za-z0-9_-]+$/);
|
|
});
|
|
|
|
it('generated states are unique across instances', () => {
|
|
const states = new Set<string>();
|
|
for (let i = 0; i < 64; i++) {
|
|
sessionStorage.clear();
|
|
const sdk = new Sdk(sdkConfig);
|
|
states.add(sdk.getOrSaveState());
|
|
}
|
|
// With 32 bytes of entropy, 64 draws collide with probability ~2^-244.
|
|
expect(states.size).toBe(64);
|
|
});
|
|
});
|
|
|
|
describe('parseAccessToken', () => {
|
|
it('should correctly parse JWT token', () => {
|
|
const sdk = new Sdk(sdkConfig);
|
|
|
|
const accessToken = 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImNlcnQtYnVpbHQtaW4iLCJ0eXAiOiJKV1QifQ.eyJvd25lciI6ImNhc2JpbiIsIm5hbWUiOiJhZG1pbiIsImNyZWF0ZWRUaW1lIjoiMjAyMC0wNy0xNlQyMTo0Njo1MiswODowMCIsInVwZGF0ZWRUaW1lIjoiMjAyNC0wMi0yMFQxMzo1MzoyNSswODowMCIsImRlbGV0ZWRUaW1lIjoiIiwiaWQiOiI5ZWIyMGY3OS0zYmI1LTRlNzQtOTlhYy0zOWUzYjlhMTcxZTgiLCJ0eXBlIjoibm9ybWFsLXVzZXIiLCJwYXNzd29yZCI6IiIsInBhc3N3b3JkU2FsdCI6IiIsInBhc3N3b3JkVHlwZSI6InBsYWluIiwiZGlzcGxheU5hbWUiOiJIZXJtYW5uIiwiZmlyc3ROYW1lIjoiIiwibGFzdE5hbWUiOiIiLCJhdmF0YXIiOiJodHRwczovL2Nkbi5jYXNiaW4uY29tL2Nhc2Rvb3IvYXZhdGFyL2Nhc2Jpbi9hZG1pbi5wbmc_dD0xNjk0MjU3ODU5ODUwOTAwMjAwIiwiYXZhdGFyVHlwZSI6IiIsInBlcm1hbmVudEF2YXRhciI6Imh0dHBzOi8vY2RuLmNhc2Jpbi5jb20vY2FzZG9vci9hdmF0YXIvY2FzYmluL2FkbWluLnBuZyIsImVtYWlsIjoiYWRtaW5AY2FzYmluLm9yZyIsImVtYWlsVmVyaWZpZWQiOmZhbHNlLCJwaG9uZSI6IiIsImNvdW50cnlDb2RlIjoiIiwicmVnaW9uIjoiVVMiLCJsb2NhdGlvbiI6IlNKQyIsImFkZHJlc3MiOltdLCJhZmZpbGlhdGlvbiI6IiIsInRpdGxlIjoiIiwiaWRDYXJkVHlwZSI6IiIsImlkQ2FyZCI6IiIsImhvbWVwYWdlIjoiIiwiYmlvIjoi5b-D54y_5LiN5a6a77yM5oSP6ams5Zub6amw77yM57qi5YyFIiwibGFuZ3VhZ2UiOiIiLCJnZW5kZXIiOiIiLCJiaXJ0aGRheSI6IiIsImVkdWNhdGlvbiI6IiIsInNjb3JlIjo5ODgyLCJrYXJtYSI6MTYwLCJyYW5raW5nIjoxMCwiaXNEZWZhdWx0QXZhdGFyIjpmYWxzZSwiaXNPbmxpbmUiOnRydWUsImlzQWRtaW4iOnRydWUsImlzRm9yYmlkZGVuIjpmYWxzZSwiaXNEZWxldGVkIjpmYWxzZSwic2lnbnVwQXBwbGljYXRpb24iOiJhcHAtY2Fzbm9kZSIsImhhc2giOiIiLCJwcmVIYXNoIjoiIiwiYWNjZXNzS2V5IjoiIiwiYWNjZXNzU2VjcmV0IjoiIiwiZ2l0aHViIjoiIiwiZ29vZ2xlIjoiIiwicXEiOiIiLCJ3ZWNoYXQiOiJveFc5TzFSMXdHbS1uZU9OcDNOU1JXM0ppVm5RIiwiZmFjZWJvb2siOiIiLCJkaW5ndGFsayI6IiIsIndlaWJvIjoiIiwiZ2l0ZWUiOiIiLCJsaW5rZWRpbiI6IiIsIndlY29tIjoiIiwibGFyayI6IiIsImdpdGxhYiI6IiIsImNyZWF0ZWRJcCI6IiIsImxhc3RTaWduaW5UaW1lIjoiIiwibGFzdFNpZ25pbklwIjoiIiwicHJlZmVycmVkTWZhVHlwZSI6IiIsInJlY292ZXJ5Q29kZXMiOm51bGwsInRvdHBTZWNyZXQiOiIiLCJtZmFQaG9uZUVuYWJsZWQiOmZhbHNlLCJtZmFFbWFpbEVuYWJsZWQiOmZhbHNlLCJsZGFwIjoiIiwicHJvcGVydGllcyI6eyJiaW8iOiIiLCJjaGVja2luRGF0ZSI6IjIwMjQwMjAzIiwiZWRpdG9yVHlwZSI6InJpY2h0ZXh0IiwiZW1haWxWZXJpZmllZFRpbWUiOiIyMDIwLTA3LTE2VDIxOjQ2OjUyKzA4OjAwIiwiZmlsZVF1b3RhIjoiNTAiLCJsYXN0QWN0aW9uRGF0ZSI6IjIwMjQtMDItMjBUMTM6NTM6MjUrMDg6MDAiLCJsb2NhdGlvbiI6IiIsIm5vIjoiMjIiLCJvYXV0aF9RUV9kaXNwbGF5TmFtZSI6IiIsIm9hdXRoX1FRX3ZlcmlmaWVkVGltZSI6IiIsIm9hdXRoX1dlQ2hhdF9hdmF0YXJVcmwiOiJodHRwczovL3RoaXJkd3gucWxvZ28uY24vbW1vcGVuL3ZpXzMyL1EwajRUd0dUZlRJUXowTWljanY3dzd4ZXUyVW5XMWRoZ0xPUHZaYkxJSmlieExLVTU2WURMcDQ3eVZROVl6dUVqMW5tYWRjYkprTnB3eWliNVd6MWZRTkp3LzEzMiIsIm9hdXRoX1dlQ2hhdF9kaXNwbGF5TmFtZSI6ImNhcm1lbiIsIm9hdXRoX1dlQ2hhdF9pZCI6Im94VzlPMVIxd0dtLW5lT05wM05TUlczSmlWblEiLCJvYXV0aF9XZUNoYXRfdXNlcm5hbWUiOiJjYXJtZW4iLCJvbmxpbmVTdGF0dXMiOiJmYWxzZSIsInBob25lVmVyaWZpZWRUaW1lIjoiIiwicmVuYW1lUXVvdGEiOiIzIiwidGFnbGluZSI6IiIsIndlYnNpdGUiOiIifSwicm9sZXMiOltdLCJwZXJtaXNzaW9ucyI6W3sib3duZXIiOiJjYXNiaW4iLCJuYW1lIjoicGVybWlzc2lvbi1jYXNpYmFzZS1hZG1pbiIsImNyZWF0ZWRUaW1lIjoiMjAyMy0wNi0yM1QwMToxNTowOSswODowMCIsImRpc3BsYXlOYW1lIjoiQ2FzaWJhc2UgQWRtaW4gUGVybWlzc2lvbiIsImRlc2NyaXB0aW9uIjoiIiwidXNlcnMiOm51bGwsImdyb3VwcyI6W10sInJvbGVzIjpbXSwiZG9tYWlucyI6WyJkZWZhdWx0Il0sIm1vZGVsIjoiRGVmYXVsdCIsImFkYXB0ZXIiOiIiLCJyZXNvdXJjZVR5cGUiOiJUcmVlTm9kZSIsInJlc291cmNlcyI6WyIvIl0sImFjdGlvbnMiOlsiQWRtaW4iXSwiZWZmZWN0IjoiQWxsb3ciLCJpc0VuYWJsZWQiOnRydWUsInN1Ym1pdHRlciI6ImFkbWluIiwiYXBwcm92ZXIiOiJhZG1pbiIsImFwcHJvdmVUaW1lIjoiMjAyMy0wNi0yM1QwMToxNTowOSswODowMCIsInN0YXRlIjoiQXBwcm92ZWQifV0sImdyb3VwcyI6W10sImxhc3RTaWduaW5Xcm9uZ1RpbWUiOiIyMDIzLTA4LTA4VDE4OjExOjA4WiIsInNpZ25pbldyb25nVGltZXMiOjAsInRva2VuVHlwZSI6ImFjY2Vzcy10b2tlbiIsInRhZyI6Ium5hem5hem5he-8jOabsumhueWQkeWkqeatjCIsInNjb3BlIjoicmVhZCIsImlzcyI6Imh0dHBzOi8vZG9vci5jYXNkb29yLmNvbSIsInN1YiI6IjllYjIwZjc5LTNiYjUtNGU3NC05OWFjLTM5ZTNiOWExNzFlOCIsImF1ZCI6WyIwYmE1MjgxMjFlYTg3YjNlYjU0ZCJdLCJleHAiOjE3MDkwMjk2OTcsIm5iZiI6MTcwODQyNDg5NywiaWF0IjoxNzA4NDI0ODk3LCJqdGkiOiJhZG1pbi9kZmUzM2Y0Ny04NGRjLTQxZjktOWE4OC03ZTU0ZWEzZTY5MjEifQ.f4l-lys7e34QEih4tJR0v5JpbIg1I8ljFoOnDnTe141UJ_ux9k2WqqCCw5g3EqwHLpiSgf_Q3ut7hgL-Ga911fLzJhSWDxx5nLfoKlQUaEu8mtz8MdleVCCytxAMxzJkeXcA7ng_QcXIFfKTRp6v5nUo8bVCp8nFfP9DJUD4irhaEwZqzJ6Y6xZRkn1YZht0j2ey39trn3cjWozKvNQNc-nSEik0UlPUO-VnQi8GnEy19C8rT6YltbboOYbmk7x57vOwecDhfUYoNdlseB3Ac3TXAHGVeCLyZWnLzU8JHNClzqqI-pUXKfQ5OGTkEBG8J2CKuzTG9cgHyAk5pA-B8Ea38rP5CNiUCbsaLR5o8bs0krJ9UJx-b52W4n8pSJmUUiE4qDCe_piesLERrTnQszFT_pG6aF_o5w0UN5Mr0houkDsqwj2sNa4oTtvkz1JuYGn1fqQ89jvPp9bGemyuI-N_gCjRecn7TKW-_1MrOYExboGCUsftY8K42PYtrpXY3hCLWx2IamMrU8fSbAqkBZym02EHEKoroCw269ejtL93ZhC6-eyljLl_Fb5NjF9infGxCRjaFco5M6k_ELKwnA5V65-OmTpT6Ti3ws6zhfs27ClZbFdtUB-HcYSMGNrqZbFdmH7ne1sKinwFsH51JAKSCCXyJOZsHMQ-RmugOAA';
|
|
const result = sdk.parseAccessToken(accessToken);
|
|
|
|
expect(result.header.alg).toEqual("RS256");
|
|
expect(result.header.kid).toEqual("cert-built-in");
|
|
expect(result.header.typ).toEqual("JWT");
|
|
expect(result.payload.owner).toEqual("casbin");
|
|
expect(result.payload.name).toEqual("admin");
|
|
});
|
|
});
|