diff --git a/EXAMPLES.md b/EXAMPLES.md index e34190c99..9f13e9e10 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -165,7 +165,7 @@ await createAuth0Client({ clientId: '', authorizationParams: { redirect_uri: '', - organization: '' + organization: '' } }); ``` @@ -176,14 +176,14 @@ You can also specify the organization when logging in: // Using a redirect await client.loginWithRedirect({ authorizationParams: { - organization: '' + organization: '' } }); // Using a popup window await client.loginWithPopup({ authorizationParams: { - organization: '' + organization: '' } }); ``` diff --git a/__tests__/Auth0Client/getTokenSilently.test.ts b/__tests__/Auth0Client/getTokenSilently.test.ts index e2cdd6f52..3df3da247 100644 --- a/__tests__/Auth0Client/getTokenSilently.test.ts +++ b/__tests__/Auth0Client/getTokenSilently.test.ts @@ -1954,7 +1954,10 @@ describe('Auth0Client', () => { }); it('stores the org_id in a hint cookie if returned in the ID token claims', async () => { - const auth0 = setup({}, { org_id: TEST_ORG_ID }); + const auth0 = setup( + { authorizationParams: { organization: TEST_ORG_ID } }, + { org_id: TEST_ORG_ID } + ); jest.spyOn(utils, 'runIframe').mockResolvedValue({ access_token: TEST_ACCESS_TOKEN, @@ -1980,7 +1983,7 @@ describe('Auth0Client', () => { ); }); - it('removes organization hint cookie if no org claim was returned in the ID token', async () => { + it('removes organization hint cookie if no organization was specified', async () => { const auth0 = setup({}); jest.spyOn(utils, 'runIframe').mockResolvedValue({ diff --git a/__tests__/Auth0Client/loginWithPopup.test.ts b/__tests__/Auth0Client/loginWithPopup.test.ts index 1149192ee..32fb32906 100644 --- a/__tests__/Auth0Client/loginWithPopup.test.ts +++ b/__tests__/Auth0Client/loginWithPopup.test.ts @@ -559,29 +559,29 @@ describe('Auth0Client', () => { ); }); - it('calls `tokenVerifier.verify` with the organization id', async () => { + it('calls `tokenVerifier.verify` with the organization', async () => { const auth0 = setup({ - authorizationParams: { organization: 'test_org_123' } + authorizationParams: { organization: 'org_123' } }); await loginWithPopup(auth0); expect(tokenVerifier).toHaveBeenCalledWith( expect.objectContaining({ - organizationId: 'test_org_123' + organization: 'org_123' }) ); }); - it('calls `tokenVerifier.verify` with the organization id given in the login method', async () => { + it('calls `tokenVerifier.verify` with the organization given in the login method', async () => { const auth0 = setup(); await loginWithPopup(auth0, { - authorizationParams: { organization: 'test_org_123' } + authorizationParams: { organization: 'org_123' } }); expect(tokenVerifier).toHaveBeenCalledWith( expect.objectContaining({ - organizationId: 'test_org_123' + organization: 'org_123' }) ); }); @@ -682,7 +682,10 @@ describe('Auth0Client', () => { it('saves organization hint cookie in storage', async () => { const auth0 = setup( - { cookieDomain: TEST_DOMAIN }, + { + cookieDomain: TEST_DOMAIN, + authorizationParams: { organization: TEST_ORG_ID } + }, { org_id: TEST_ORG_ID } ); diff --git a/__tests__/Auth0Client/loginWithRedirect.test.ts b/__tests__/Auth0Client/loginWithRedirect.test.ts index 46ee9accd..f399b1736 100644 --- a/__tests__/Auth0Client/loginWithRedirect.test.ts +++ b/__tests__/Auth0Client/loginWithRedirect.test.ts @@ -452,22 +452,25 @@ describe('Auth0Client', () => { ); }); - it('calls `tokenVerifier.verify` with the global organization id', async () => { + it('calls `tokenVerifier.verify` with the global organization', async () => { const auth0 = setup({ - authorizationParams: { organization: 'test_org_123' } + authorizationParams: { organization: 'org_123' } }); await loginWithRedirect(auth0); expect(tokenVerifier).toHaveBeenCalledWith( expect.objectContaining({ - organizationId: 'test_org_123' + organization: 'org_123' }) ); }); - it('stores the organization ID in a hint cookie', async () => { - const auth0 = setup({}, { org_id: TEST_ORG_ID }); + it('stores the organization in a hint cookie', async () => { + const auth0 = setup( + { authorizationParams: { organization: TEST_ORG_ID } }, + { org_id: TEST_ORG_ID } + ); await loginWithRedirect(auth0); @@ -488,7 +491,8 @@ describe('Auth0Client', () => { ); }); - it('removes the org hint cookie if no org_id claim in the ID token', async () => { + it('removes the organization hint cookie if no organization specified', async () => { + // TODO: WHAT IS ORG_NAME ? const auth0 = setup({}); await loginWithRedirect(auth0); @@ -504,9 +508,9 @@ describe('Auth0Client', () => { ); }); - it('calls `tokenVerifier.verify` with the specific organization id', async () => { + it('calls `tokenVerifier.verify` with the specific organization', async () => { const auth0 = setup({ - authorizationParams: { organization: 'test_org_123' } + authorizationParams: { organization: 'org_123' } }); await loginWithRedirect(auth0, { @@ -514,7 +518,7 @@ describe('Auth0Client', () => { }); expect(tokenVerifier).toHaveBeenCalledWith( expect.objectContaining({ - organizationId: 'test_org_456' + organization: 'test_org_456' }) ); }); diff --git a/__tests__/jwt.test.ts b/__tests__/jwt.test.ts index 597c6c11e..4e6a363e1 100644 --- a/__tests__/jwt.test.ts +++ b/__tests__/jwt.test.ts @@ -153,14 +153,62 @@ describe('jwt', () => { }); it('verifies correctly with an organization ID', async () => { - const org_id = 'test_org_123'; + const org_id = 'org_123'; const id_token = await createJWT({ ...DEFAULT_PAYLOAD, org_id }); const { encoded, header, claims } = verify({ ...verifyOptions, id_token, - organizationId: org_id + organization: org_id + }); + + expect({ encoded, header, payload: claims }).toMatchObject( + verifier.decode(id_token) + ); + }); + + it('verifies correctly with an organization Name', async () => { + const org_name = 'my-org'; + + const id_token = await createJWT({ ...DEFAULT_PAYLOAD, org_name }); + + const { encoded, header, claims } = verify({ + ...verifyOptions, + id_token, + organization: org_name + }); + + expect({ encoded, header, payload: claims }).toMatchObject( + verifier.decode(id_token) + ); + }); + + it('verifies correctly with an organization Name in wrong case', async () => { + const org_name = 'my-org'; + + const id_token = await createJWT({ ...DEFAULT_PAYLOAD, org_name }); + + const { encoded, header, claims } = verify({ + ...verifyOptions, + id_token, + organization: 'My-org' + }); + + expect({ encoded, header, payload: claims }).toMatchObject( + verifier.decode(id_token) + ); + }); + + it('verifies correctly with an organization Name surrounded by whitespace', async () => { + const org_name = 'my-org'; + + const id_token = await createJWT({ ...DEFAULT_PAYLOAD, org_name }); + + const { encoded, header, claims } = verify({ + ...verifyOptions, + id_token, + organization: ' my-org ' }); expect({ encoded, header, payload: claims }).toMatchObject( @@ -369,26 +417,49 @@ describe('jwt', () => { ).not.toThrow(); }); - it('validate org_id is present when organizationId is provided', async () => { + it('validate org_id is present when organization id is provided', async () => { const id_token = await createJWT({ ...DEFAULT_PAYLOAD }); expect(() => - verify({ ...verifyOptions, id_token, organizationId: 'test_org_123' }) + verify({ ...verifyOptions, id_token, organization: 'org_123' }) ).toThrow( 'Organization ID (org_id) claim must be a string present in the ID token' ); }); - it('validate org_id matches the claim when organizationId is provided', async () => { + it('validate org_id matches the claim when organization id is provided', async () => { const id_token = await createJWT({ ...DEFAULT_PAYLOAD, org_id: 'test_org_456' }); expect(() => - verify({ ...verifyOptions, id_token, organizationId: 'test_org_123' }) + verify({ ...verifyOptions, id_token, organization: 'org_123' }) + ).toThrow( + 'Organization ID (org_id) claim mismatch in the ID token; expected "org_123", found "test_org_456"' + ); + }); + + it('validate org_name is present when organization name is provided', async () => { + const id_token = await createJWT({ ...DEFAULT_PAYLOAD }); + + expect(() => + verify({ ...verifyOptions, id_token, organization: 'my-org' }) + ).toThrow( + 'Organization Name (org_name) claim must be a string present in the ID token' + ); + }); + + it('validate org_id matches the claim when organization id is provided', async () => { + const id_token = await createJWT({ + ...DEFAULT_PAYLOAD, + org_name: 'my-other-org' + }); + + expect(() => + verify({ ...verifyOptions, id_token, organization: 'my-org' }) ).toThrow( - 'Organization ID (org_id) claim mismatch in the ID token; expected "test_org_123", found "test_org_456"' + 'Organization Name (org_name) claim mismatch in the ID token; expected "my-org", found "my-other-org"' ); }); }); diff --git a/src/Auth0Client.ts b/src/Auth0Client.ts index 78d658af9..5fd0085ae 100644 --- a/src/Auth0Client.ts +++ b/src/Auth0Client.ts @@ -208,7 +208,7 @@ export class Auth0Client { this.transactionManager = new TransactionManager( transactionStorage, this.options.clientId, - this.options.cookieDomain, + this.options.cookieDomain ); this.nowProvider = this.options.nowProvider || DEFAULT_NOW_PROVIDER; @@ -249,7 +249,7 @@ export class Auth0Client { private async _verifyIdToken( id_token: string, nonce?: string, - organizationId?: string + organization?: string ) { const now = await this.nowProvider(); @@ -258,16 +258,16 @@ export class Auth0Client { aud: this.options.clientId, id_token, nonce, - organizationId, + organization, leeway: this.options.leeway, max_age: parseNumber(this.options.authorizationParams.max_age), now }); } - private _processOrgIdHint(organizationId?: string) { - if (organizationId) { - this.cookieStorage.save(this.orgHintCookieName, organizationId, { + private _processOrgHint(organization?: string) { + if (organization) { + this.cookieStorage.save(this.orgHintCookieName, organization, { daysUntilExpire: this.sessionCheckExpiryDays, cookieDomain: this.options.cookieDomain }); @@ -383,7 +383,7 @@ export class Auth0Client { throw new GenericError('state_mismatch', 'Invalid state'); } - const organizationId = + const organization = options.authorizationParams?.organization || this.options.authorizationParams.organization; @@ -398,7 +398,7 @@ export class Auth0Client { }, { nonceIn: params.nonce, - organizationId + organization } ); } @@ -449,7 +449,7 @@ export class Auth0Client { const { openUrl, fragment, appState, ...urlOptions } = patchOpenUrlWithOnRedirect(options); - const organizationId = + const organization = urlOptions.authorizationParams?.organization || this.options.authorizationParams.organization; @@ -460,7 +460,7 @@ export class Auth0Client { this.transactionManager.create({ ...transaction, appState, - ...(organizationId && { organizationId }) + ...(organization && { organization }) }); const urlWithFragment = fragment ? `${url}#${fragment}` : url; @@ -516,7 +516,7 @@ export class Auth0Client { throw new GenericError('state_mismatch', 'Invalid state'); } - const organizationId = transaction.organizationId; + const organization = transaction.organization; const nonceIn = transaction.nonce; const redirect_uri = transaction.redirect_uri; @@ -529,7 +529,7 @@ export class Auth0Client { code: code as string, ...(redirect_uri ? { redirect_uri } : {}) }, - { nonceIn, organizationId } + { nonceIn, organization } ); return { @@ -862,10 +862,10 @@ export class Auth0Client { prompt: 'none' }; - const orgIdHint = this.cookieStorage.get(this.orgHintCookieName); + const orgHint = this.cookieStorage.get(this.orgHintCookieName); - if (orgIdHint && !params.organization) { - params.organization = orgIdHint; + if (orgHint && !params.organization) { + params.organization = orgHint; } const { @@ -912,7 +912,8 @@ export class Auth0Client { timeout: options.authorizationParams.timeout || this.httpTimeoutMs }, { - nonceIn + nonceIn, + organization: params.organization } ); @@ -1095,7 +1096,7 @@ export class Auth0Client { options: PKCERequestTokenOptions | RefreshTokenRequestTokenOptions, additionalParameters?: RequestTokenAdditionalParameters ) { - const { nonceIn, organizationId } = additionalParameters || {}; + const { nonceIn, organization } = additionalParameters || {}; const authResult = await oauthToken( { baseUrl: this.domainUrl, @@ -1111,7 +1112,7 @@ export class Auth0Client { const decodedToken = await this._verifyIdToken( authResult.id_token, nonceIn, - organizationId + organization ); await this._saveEntryInCache({ @@ -1128,7 +1129,7 @@ export class Auth0Client { cookieDomain: this.options.cookieDomain }); - this._processOrgIdHint(decodedToken.claims.org_id); + this._processOrgHint(organization); return { ...authResult, decodedToken }; } @@ -1154,5 +1155,5 @@ interface RefreshTokenRequestTokenOptions extends BaseRequestTokenOptions { interface RequestTokenAdditionalParameters { nonceIn?: string; - organizationId?: string; + organization?: string; } diff --git a/src/global.ts b/src/global.ts index b51ffe747..ca29c3e61 100644 --- a/src/global.ts +++ b/src/global.ts @@ -78,10 +78,13 @@ export interface AuthorizationParams { connection?: string; /** - * The Id of an organization to log in to. + * The organization to log in to. + * + * This will specify an `organization` parameter in your user's login request. + * + * - If you provide an Organization ID (a string with the prefix `org_`), it will be validated against the `org_id` claim of your user's ID Token. The validation is case-sensitive. + * - If you provide an Organization Name (a string *without* the prefix `org_`), it will be validated against the `org_name` claim of your user's ID Token. The validation is case-insensitive. * - * This will specify an `organization` parameter in your user's login request and will add a step to validate - * the `org_id` claim in your user's ID Token. */ organization?: string; @@ -551,7 +554,7 @@ export interface JWTVerifyOptions { nonce?: string; leeway?: number; max_age?: number; - organizationId?: string; + organization?: string; now?: number; } @@ -593,6 +596,7 @@ export interface IdToken { cnf?: string; sid?: string; org_id?: string; + org_name?: string; [key: string]: any; } diff --git a/src/jwt.ts b/src/jwt.ts index 9e679ebf6..81a8ba029 100644 --- a/src/jwt.ts +++ b/src/jwt.ts @@ -193,15 +193,31 @@ export const verify = (options: JWTVerifyOptions) => { } } - if (options.organizationId) { - if (!decoded.claims.org_id) { - throw new Error( - 'Organization ID (org_id) claim must be a string present in the ID token' - ); - } else if (options.organizationId !== decoded.claims.org_id) { - throw new Error( - `Organization ID (org_id) claim mismatch in the ID token; expected "${options.organizationId}", found "${decoded.claims.org_id}"` - ); + if (options.organization) { + const org = options.organization.trim(); + if (org.startsWith('org_')) { + const orgId = org; + if (!decoded.claims.org_id) { + throw new Error( + 'Organization ID (org_id) claim must be a string present in the ID token' + ); + } else if (orgId !== decoded.claims.org_id) { + throw new Error( + `Organization ID (org_id) claim mismatch in the ID token; expected "${orgId}", found "${decoded.claims.org_id}"` + ); + } + } else { + const orgName = org.toLowerCase(); + // TODO should we verify if there is an `org_id` claim? + if (!decoded.claims.org_name) { + throw new Error( + 'Organization Name (org_name) claim must be a string present in the ID token' + ); + } else if (orgName !== decoded.claims.org_name.toLowerCase()) { + throw new Error( + `Organization Name (org_name) claim mismatch in the ID token; expected "${orgName}", found "${decoded.claims.org_name.toLowerCase()}"` + ); + } } } diff --git a/src/transaction-manager.ts b/src/transaction-manager.ts index 40efc450c..31f97a5b2 100644 --- a/src/transaction-manager.ts +++ b/src/transaction-manager.ts @@ -9,7 +9,7 @@ interface Transaction { appState?: any; code_verifier: string; redirect_uri?: string; - organizationId?: string; + organization?: string; state?: string; } diff --git a/static/index.html b/static/index.html index 29b35eda5..72c42dd6c 100644 --- a/static/index.html +++ b/static/index.html @@ -224,7 +224,7 @@

Last error

v-model="organization" class="form-control" id="organization" - data-cy="organizationId" + data-cy="organization" />