Skip to content

Commit

Permalink
Fixed missing endpoints for feature Organizations for Client Credenti…
Browse files Browse the repository at this point in the history
…als changes (#1046)
tusharpandey13 authored Nov 5, 2024
1 parent edef4d1 commit 8c440b2
Showing 9 changed files with 260 additions and 118 deletions.
7 changes: 0 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -56,10 +56,3 @@ jobs:
- name: Tests
shell: bash
run: npm run test:ci

- name: Upload coverage
if: matrix.node-version == '18.17'
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # pin@4.5.0
with:
token: ${{ secrets.CODECOV_TOKEN }}

12 changes: 0 additions & 12 deletions src/auth/id-token-validator.ts
Original file line number Diff line number Diff line change
@@ -102,24 +102,12 @@ export class IDTokenValidator {
'Organization Id (org_id) claim must be a string present in the ID token'
);
}

if (payload.org_id !== organization) {
throw new Error(
`Organization Id (org_id) claim value mismatch in the ID token; expected "${organization}", found "${payload.org_id}"'`
);
}
} else {
if (!payload.org_name || typeof payload.org_name !== 'string') {
throw new Error(
'Organization Name (org_name) claim must be a string present in the ID token'
);
}

if (payload.org_name !== organization.toLowerCase()) {
throw new Error(
`Organization Name (org_name) claim value mismatch in the ID token; expected "${organization}", found "${payload.org_name}"'`
);
}
}
}

61 changes: 60 additions & 1 deletion src/management/__generated/managers/organizations-manager.ts
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import type {
GetInvitations200ResponseOneOfInner,
GetMembers200Response,
GetOrganizationClientGrants200Response,
GetOrganizationClientGrants200ResponseOneOfInner,
GetOrganizationMemberRoles200Response,
GetOrganizations200Response,
GetOrganizations200ResponseOneOfInner,
@@ -17,6 +18,7 @@ import type {
PostEnabledConnectionsRequest,
PostInvitationsRequest,
PostMembersRequest,
PostOrganizationClientGrantsRequest,
PostOrganizationMemberRolesRequest,
PostOrganizations201Response,
PostOrganizationsRequest,
@@ -25,10 +27,10 @@ import type {
GetMembers200ResponseOneOf,
GetMembers200ResponseOneOfInner,
GetOrganizationClientGrants200ResponseOneOf,
GetOrganizationClientGrants200ResponseOneOfInner,
GetOrganizationMemberRoles200ResponseOneOf,
GetOrganizationMemberRoles200ResponseOneOfInner,
GetOrganizations200ResponseOneOf,
DeleteClientGrantsByGrantIdRequest,
DeleteEnabledConnectionsByConnectionIdRequest,
DeleteInvitationsByInvitationIdRequest,
DeleteMembersOperationRequest,
@@ -49,6 +51,7 @@ import type {
PostEnabledConnectionsOperationRequest,
PostInvitationsOperationRequest,
PostMembersOperationRequest,
PostOrganizationClientGrantsOperationRequest,
PostOrganizationMemberRolesOperationRequest,
} from '../models/index.js';

@@ -58,6 +61,30 @@ const { BaseAPI } = runtime;
*
*/
export class OrganizationsManager extends BaseAPI {
/**
* Remove a client grant from an organization
*
* @throws {RequiredError}
*/
async deleteClientGrantsByGrantId(
requestParameters: DeleteClientGrantsByGrantIdRequest,
initOverrides?: InitOverride
): Promise<ApiResponse<void>> {
runtime.validateRequiredRequestParams(requestParameters, ['id', 'grant_id']);

const response = await this.request(
{
path: `/organizations/{id}/client-grants/{grant_id}`
.replace('{id}', encodeURIComponent(String(requestParameters.id)))
.replace('{grant_id}', encodeURIComponent(String(requestParameters.grant_id))),
method: 'DELETE',
},
initOverrides
);

return runtime.VoidApiResponse.fromResponse(response);
}

/**
* Delete connections from an organization
*
@@ -859,6 +886,38 @@ export class OrganizationsManager extends BaseAPI {
return runtime.VoidApiResponse.fromResponse(response);
}

/**
* Associate a client grant with an organization
*
* @throws {RequiredError}
*/
async postOrganizationClientGrants(
requestParameters: PostOrganizationClientGrantsOperationRequest,
bodyParameters: PostOrganizationClientGrantsRequest,
initOverrides?: InitOverride
): Promise<ApiResponse<GetOrganizationClientGrants200ResponseOneOfInner>> {
runtime.validateRequiredRequestParams(requestParameters, ['id']);

const headerParameters: runtime.HTTPHeaders = {};

headerParameters['Content-Type'] = 'application/json';

const response = await this.request(
{
path: `/organizations/{id}/client-grants`.replace(
'{id}',
encodeURIComponent(String(requestParameters.id))
),
method: 'POST',
headers: headerParameters,
body: bodyParameters,
},
initOverrides
);

return runtime.JSONApiResponse.fromResponse(response);
}

/**
* Assign one or more roles to a given user that will be applied in the context of the provided organization
*
35 changes: 35 additions & 0 deletions src/management/__generated/models/index.ts
Original file line number Diff line number Diff line change
@@ -10513,6 +10513,16 @@ export interface PostMembersRequest {
*/
members: Array<string>;
}
/**
*
*/
export interface PostOrganizationClientGrantsRequest {
/**
* A Client Grant ID to add to the organization.
*
*/
grant_id: string;
}
/**
*
*/
@@ -14924,6 +14934,21 @@ export interface GetLogsByIdRequest {
*/
id: string;
}
/**
*
*/
export interface DeleteClientGrantsByGrantIdRequest {
/**
* Organization identifier
*
*/
id: string;
/**
* The Client Grant ID to remove from the organization
*
*/
grant_id: string;
}
/**
*
*/
@@ -15319,6 +15344,16 @@ export interface PostMembersOperationRequest {
*/
id: string;
}
/**
*
*/
export interface PostOrganizationClientGrantsOperationRequest {
/**
* Organization identifier
*
*/
id: string;
}
/**
*
*/
56 changes: 0 additions & 56 deletions test/auth/id-token-validator.test.ts
Original file line number Diff line number Diff line change
@@ -386,60 +386,4 @@ describe('id-token-validator', () => {
'Organization Name (org_name) claim must be a string present in the ID token'
);
});

it('should throw when org id claim doesnt match org expected', async () => {
const idTokenValidator = new IDTokenValidator({
domain: DOMAIN,
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
});

const jwt = await sign({ payload: { org_id: 'org_1234' } });

await expect(idTokenValidator.validate(jwt, { organization: 'org_123' })).rejects.toThrow(
'Organization Id (org_id) claim value mismatch in the ID token; expected "org_123", found "org_1234'
);
});

it('should throw when org name claim doesnt match org expected', async () => {
const idTokenValidator = new IDTokenValidator({
domain: DOMAIN,
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
});

const jwt = await sign({ payload: { org_name: 'notExpectedOrg' } });

await expect(idTokenValidator.validate(jwt, { organization: 'testorg' })).rejects.toThrow(
'Organization Name (org_name) claim value mismatch in the ID token; expected "testorg", found "notExpectedOrg'
);
});

it('should NOT throw when org_id matches expected organization', async () => {
const idTokenValidator = new IDTokenValidator({
domain: DOMAIN,
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
});

const jwt = await sign({ payload: { org_id: 'org_123' } });

await expect(
idTokenValidator.validate(jwt, { organization: 'org_123' })
).resolves.not.toThrow();
});

it('should NOT throw when org_name matches expected organization', async () => {
const idTokenValidator = new IDTokenValidator({
domain: DOMAIN,
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
});

const jwt = await sign({ payload: { org_name: 'testorg' } });

await expect(
idTokenValidator.validate(jwt, { organization: 'testOrg' })
).resolves.not.toThrow();
});
});
34 changes: 0 additions & 34 deletions test/auth/oauth.test.ts
Original file line number Diff line number Diff line change
@@ -409,38 +409,4 @@ describe('OAuth (with ID Token validation)', () => {
);
nockDone();
});

it('should throw for invalid organization id', async () => {
const { nockDone } = await nockBack('auth/fixtures/oauth.json', {
before: await withIdToken({
...opts,
payload: { org_id: 'org_123' },
}),
});
const oauth = new OAuth(opts);
await expect(
oauth.refreshTokenGrant(
{ refresh_token: 'test-refresh-token' },
{ idTokenValidateOptions: { organization: 'org_1235' } }
)
).rejects.toThrowError(/\(org_id\) claim value mismatch in the ID token/);
nockDone();
});

it('should throw for invalid organization name', async () => {
const { nockDone } = await nockBack('auth/fixtures/oauth.json', {
before: await withIdToken({
...opts,
payload: { org_name: 'org123' },
}),
});
const oauth = new OAuth(opts);
await expect(
oauth.refreshTokenGrant(
{ refresh_token: 'test-refresh-token' },
{ idTokenValidateOptions: { organization: 'org1235' } }
)
).rejects.toThrowError(/\(org_name\) claim value mismatch in the ID token/);
nockDone();
});
});
47 changes: 39 additions & 8 deletions test/management/organizations.test.ts
Original file line number Diff line number Diff line change
@@ -9,21 +9,21 @@ import {
GetOrganizationClientGrantsRequest,
GetOrganizationClientGrants200Response,
ApiResponse,
DeleteClientGrantsByGrantIdRequest,
GetOrganizationClientGrants200ResponseOneOfInner,
} from '../../src/index.js';

describe('OrganizationsManager', () => {
let organizations: OrganizationsManager;
import { checkMethod } from '../utils/index.js';

describe('OrganizationsManager', () => {
let request: nock.Scope;
const token = 'TOKEN';

beforeAll(() => {
const client = new ManagementClient({
domain: 'tenant.auth0.com',
token: token,
});
organizations = client.organizations;
const client = new ManagementClient({
domain: 'tenant.auth0.com',
token: token,
});
const organizations: OrganizationsManager = client.organizations;

describe('#constructor', () => {
it('should throw an error when no base URL is provided', () => {
@@ -1474,4 +1474,35 @@ describe('OrganizationsManager', () => {
});
});
});

describe('#deleteClientGrantsById', () => {
const requestParameters: DeleteClientGrantsByGrantIdRequest = {
id: 'org_123',
grant_id: 'grant_id',
};
const operation = organizations.deleteClientGrantsByGrantId(requestParameters);
const expectedResponse = undefined;
const uri = `/organizations/{id}/client-grants/{grant_id}`
.replace('{id}', encodeURIComponent(String(requestParameters.id)))
.replace('{grant_id}', encodeURIComponent(String(requestParameters.grant_id)));
const method = 'delete';

checkMethod({ operation, expectedResponse, uri, method });
});

describe('#postOrganizationClientGrants', () => {
const requestParameters = { id: 'org_123' };
const requestBody = { grant_id: 'grant_id' };
const operation = organizations.postOrganizationClientGrants(requestParameters, requestBody);
const expectedResponse: GetOrganizationClientGrants200ResponseOneOfInner = <
GetOrganizationClientGrants200ResponseOneOfInner
>{};
const uri = `/organizations/{id}/client-grants`.replace(
'{id}',
encodeURIComponent(String(requestParameters.id))
);
const method = 'post';

checkMethod({ operation, expectedResponse, uri, method, requestBody });
});
});
1 change: 1 addition & 0 deletions test/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './extractParts.js';
export * from './withIdToken.js';
export * from './wrapperTestUtils.js';
125 changes: 125 additions & 0 deletions test/utils/wrapperTestUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// tests.util.ts
// this file contains commmon test functions that are used to test sdk endpoints
// since the management api managers are essentially wrappers around the REST endpoints,
// these functions end up being repeated for all the managers, this file aims to reduce repetition
// it performs basic sanity checks, input output checks and error handling checks

import nock, { RequestBodyMatcher } from 'nock';
import { ApiResponse } from '../../src/lib/models.js';

const DOMAIN = `tenant.auth0.com`;
const API_URL = `https://${DOMAIN}/api/v2`;

// this is not technically not required as type checking will automatically check fot this
// but including it for the sake of completeness
/**
* Checks if the given operation returns a promise when no callback is provided.
*
* @template T - The type of the response expected from the promise.
* @param {any | Promise<ApiResponse<T>>} operation - The operation to check, which can be either a promise or any other type.
* @returns {void}
*/
export function checkForPromise<T>(operation: any | Promise<ApiResponse<T>>): void {
it('should return a promise if no callback is given', (done) => {
expect(operation instanceof Promise).toBeTruthy();
operation.then(done.bind(null, null)).catch(done.bind(null, null));
});
}

/**
* Utility function to test if an operation correctly handles errors.
*
* @template T - The type of the response expected from the operation.
* @param {Promise<ApiResponse<T>>} operation - The promise representing the operation to be tested.
* @returns {void} - This function does not return anything.
*
* @example
* ```typescript
* checkErrorHandler(someApiOperation);
* ```
*/
export function checkErrorHandler<T>(operation: Promise<ApiResponse<T>>): void {
it('should pass any errors to the promise catch handler', () => {
nock.cleanAll();

return operation.catch((err) => {
expect(err).toBeDefined();
});
});
}

/**
* Verifies that a given operation makes a request to the specified endpoint.
*
* @template T - The type of the result of the operation.
* @param operation - A promise representing the operation to be checked.
* @param request - The nock scope representing the expected request.
*/
export function checkRequestInterceptor<T>(operation: Promise<T>, request: nock.Scope): void {
it(`should make a request to the endpoint`, async () => {
await operation;
expect(request.isDone()).toBeTruthy();
});
}

/**
* Tests an asynchronous operation by comparing its result to an expected response.
*
* @template T - The type of the expected response data.
* @param {Promise<ApiResponse<T>>} operation - The asynchronous operation to be tested.
* @param {T} expectedResponse - The expected response data to compare against the operation's result.
*/
export function checkOperation<T>(operation: Promise<ApiResponse<T>>, expectedResponse: T): void {
it('should test the method', async () => {
const result = await operation;
expect(result.data).toEqual(expectedResponse);
});
}

export type CheckMethodParams<T> = {
operation: Promise<ApiResponse<T>>;
expectedResponse: any;
uri: string | RegExp | { (uri: string): boolean };
method: string;
requestBody?: RequestBodyMatcher | any;
};

// this function combines the above functions to check an SDK manager method.
/**
* Checks the given manager method by intercepting the request and validating the response.
*
* Following checks are performed:
* 1. The operation is a promise.
* 2. The operation is rejected in case of an error.
* 3. The request is made to the specified endpoint in the given method.
* 4. The response from the operation is as expected.
*
* @template T - The type of the expected response.
* @param {Object} params - The parameters for the checkMethod function.
* @param {Promise<ApiResponse<T>>} params.operation - The operation to be tested.
* @param {any} params.expectedResponse - The expected response from the operation.
* @param {string | RegExp | ((uri: string) => boolean)} params.uri - The URI to intercept.
* @param {string} params.method - The HTTP method to intercept (e.g., 'GET', 'POST').
* @param {RequestBodyMatcher | any} [params.requestBody] - The optional request body to match.
*/
export const checkMethod = <T>({
operation,
expectedResponse,
uri,
method,
requestBody,
}: CheckMethodParams<T>): void => {
// nock the API with success scenario
let request: nock.Scope = nock(API_URL)
.intercept(uri, method, requestBody)
.reply(200, expectedResponse);

// check for various success checks
checkForPromise(operation);
checkRequestInterceptor(operation, request);
checkOperation(operation, expectedResponse);

// nock the API with error scenario
request = nock(API_URL).intercept(uri, method, requestBody).reply(500);
checkErrorHandler(operation);
};

0 comments on commit 8c440b2

Please sign in to comment.