Skip to content

Commit b432427

Browse files
committed
Validate 'aud' in DID Token. Skip attachment validation if 'none'.
1 parent e6a70c7 commit b432427

File tree

12 files changed

+168
-10
lines changed

12 files changed

+168
-10
lines changed

CHANGELOG.md

+16
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
# v2.0.0 (Wed May 03 2023)
2+
3+
#### 🚀 Enhancement
4+
5+
- Add Magic Connect Admin SDK support for Token Resource [#111](https://github.com/magiclabs/magic-admin-js/pull/111) ([@magic-ravi](https://github.com/magic-ravi))
6+
- [Security Enhancement]: Validate `aud` using Magic client ID.
7+
- Add new `init` async method to create Magic instance and pull client ID from Magic servers if not provided.
8+
- Deprecate regular constructor.
9+
- Skip validation of attachment if 'none' is passed in `validate`.
10+
11+
#### Authors: 1
12+
13+
- Ravi Bhankharia ([@magic-ravi](https://github.com/magic-ravi))
14+
15+
---
16+
117
# v1.10.0 (Wed May 03 2023)
218

319
#### 🚀 Enhancement

README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ Sign up or log in to the [developer dashboard](https://dashboard.magic.link) to
3232
```ts
3333
const { Magic } = require('@magic-sdk/admin');
3434

35-
const magic = new Magic('YOUR_SECRET_API_KEY');
36-
35+
// In async function:
36+
const magic = await Magic.init('YOUR_SECRET_API_KEY');
37+
// OR
38+
Magic.init('YOUR_SECRET_API_KEY').then((magic) => magic);
3739
// Read the docs to learn about next steps! 🚀
3840
```

src/core/sdk-exceptions.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function createFailedRecoveringProofError() {
4141
export function createApiKeyMissingError() {
4242
return new MagicAdminSDKError(
4343
ErrorCode.ApiKeyMissing,
44-
'Please provide a secret Fortmatic API key that you acquired from the developer dashboard.',
44+
'Please provide a secret Magic API key that you acquired from the developer dashboard.',
4545
);
4646
}
4747

@@ -63,3 +63,10 @@ export function createExpectedBearerStringError() {
6363
'Expected argument to be a string in the `Bearer {token}` format.',
6464
);
6565
}
66+
67+
export function createAudienceMismatchError() {
68+
return new MagicAdminSDKError(
69+
ErrorCode.AudienceMismatch,
70+
'Audience does not match client ID. Please ensure your secret key matches the application which generated the DID token.',
71+
);
72+
}

src/core/sdk.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { TokenModule } from '../modules/token';
33
import { UsersModule } from '../modules/users';
44
import { UtilsModule } from '../modules/utils';
55
import { MagicAdminSDKAdditionalConfiguration } from '../types';
6+
import { get } from '../utils/rest';
7+
import { createApiKeyMissingError } from './sdk-exceptions';
68

79
export class MagicAdminSDK {
810
public readonly apiBaseUrl: string;
@@ -24,13 +26,42 @@ export class MagicAdminSDK {
2426
*/
2527
public readonly utils: UtilsModule;
2628

29+
/**
30+
* Unique client identifier
31+
*/
32+
public clientId: string | null;
33+
34+
/**
35+
* Deprecated. Use `init` instead.
36+
* @param secretApiKey
37+
* @param options
38+
*/
2739
constructor(public readonly secretApiKey?: string, options?: MagicAdminSDKAdditionalConfiguration) {
2840
const endpoint = options?.endpoint ?? 'https://api.magic.link';
2941
this.apiBaseUrl = endpoint.replace(/\/+$/, '');
30-
42+
this.clientId = options?.clientId ?? null;
3143
// Assign API Modules
3244
this.token = new TokenModule(this);
3345
this.users = new UsersModule(this);
3446
this.utils = new UtilsModule(this);
3547
}
48+
49+
public static async init(secretApiKey?: string, options?: MagicAdminSDKAdditionalConfiguration) {
50+
if (!secretApiKey) throw createApiKeyMissingError();
51+
52+
let hydratedOptions = options ?? {};
53+
54+
const endpoint = hydratedOptions.endpoint ?? 'https://api.magic.link';
55+
const apiBaseUrl = endpoint.replace(/\/+$/, '');
56+
57+
if (!hydratedOptions.clientId) {
58+
const resp = await get<{
59+
client_id: string | null;
60+
app_scope: string | null;
61+
}>(`${apiBaseUrl}/v1/admin/client/get`, secretApiKey);
62+
hydratedOptions = { ...hydratedOptions, clientId: resp.client_id };
63+
}
64+
65+
return new MagicAdminSDK(secretApiKey, hydratedOptions);
66+
}
3667
}

src/modules/token/index.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
createTokenExpiredError,
88
createMalformedTokenError,
99
createTokenCannotBeUsedYetError,
10+
createAudienceMismatchError,
1011
} from '../../core/sdk-exceptions';
1112
import { ecRecover } from '../../utils/ec-recover';
1213
import { parseDIDToken } from '../../utils/parse-didt';
@@ -15,7 +16,7 @@ import { parsePublicAddressFromIssuer } from '../../utils/issuer';
1516
export class TokenModule extends BaseModule {
1617
public validate(DIDToken: string, attachment = 'none') {
1718
let tokenSigner = '';
18-
let attachmentSigner = '';
19+
let attachmentSigner: string | null = null;
1920
let claimedIssuer = '';
2021
let parsedClaim;
2122
let proof: string;
@@ -35,13 +36,15 @@ export class TokenModule extends BaseModule {
3536
tokenSigner = ecRecover(claim, proof).toLowerCase();
3637

3738
// Recover the attachment signer
38-
attachmentSigner = ecRecover(attachment, parsedClaim.add).toLowerCase();
39+
if (attachment && attachment !== 'none') {
40+
attachmentSigner = ecRecover(attachment, parsedClaim.add).toLowerCase();
41+
}
3942
} catch {
4043
throw createFailedRecoveringProofError();
4144
}
4245

4346
// Assert the expected signer
44-
if (claimedIssuer !== tokenSigner || claimedIssuer !== attachmentSigner) {
47+
if (claimedIssuer !== tokenSigner || (attachmentSigner && claimedIssuer !== attachmentSigner)) {
4548
throw createIncorrectSignerAddressError();
4649
}
4750

@@ -57,6 +60,11 @@ export class TokenModule extends BaseModule {
5760
if (parsedClaim.nbf - nbfLeeway > timeSecs) {
5861
throw createTokenCannotBeUsedYetError();
5962
}
63+
64+
// Assert the audience matches the client ID.
65+
if (this.sdk.clientId && parsedClaim.aud !== this.sdk.clientId) {
66+
throw createAudienceMismatchError();
67+
}
6068
}
6169

6270
public decode(DIDToken: string): ParsedDIDToken {

src/types/exception-types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ export enum ErrorCode {
88
MalformedTokenError = 'ERROR_MALFORMED_TOKEN',
99
ServiceError = 'SERVICE_ERROR',
1010
ExpectedBearerString = 'EXPECTED_BEARER_STRING',
11+
AudienceMismatch = 'ERROR_AUDIENCE_MISMATCH',
1112
}

src/types/sdk-types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface MagicAdminSDKAdditionalConfiguration {
22
endpoint?: string;
3+
clientId?: string | null;
34
}
45

56
export interface MagicWallet {

test/lib/constants.ts

+3
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,6 @@ export const INVALID_SIGNER_DIDT =
4444

4545
export const EXPIRED_DIDT =
4646
'WyIweGE3MDUzYzg3OTI2ZjMzZDBjMTZiMjMyYjYwMWYxZDc2NmRiNWY3YWM4MTg2MzUyMzY4ZjAyMzIyMGEwNzJjYzkzM2JjYjI2MmU4ODQyNWViZDA0MzcyZGU3YTc0NzMwYjRmYWYzOGU0ZjgwNmYzOTJjMTVkNzY2YmVkMjVlZmUxMWIiLCJ7XCJpYXRcIjoxNTg1MDEwODM1LFwiZXh0XCI6MTU4NTAxMDgzNixcImlzc1wiOlwiZGlkOmV0aHI6MHhCMmVjOWI2MTY5OTc2MjQ5MWI2NTQyMjc4RTlkRkVDOTA1MGY4MDg5XCIsXCJzdWJcIjpcIjZ0RlhUZlJ4eWt3TUtPT2pTTWJkUHJFTXJwVWwzbTNqOERReWNGcU8ydHc9XCIsXCJhdWRcIjpcImRpZDptYWdpYzpkNGMwMjgxYi04YzViLTQ5NDMtODUwOS0xNDIxNzUxYTNjNzdcIixcIm5iZlwiOjE1ODUwMTA4MzUsXCJ0aWRcIjpcImFjMmE4YzFjLWE4OWEtNDgwOC1hY2QxLWM1ODg1ZTI2YWZiY1wiLFwiYWRkXCI6XCIweDkxZmJlNzRiZTZjNmJmZDhkZGRkZDkzMDExYjA1OWI5MjUzZjEwNzg1NjQ5NzM4YmEyMTdlNTFlMGUzZGYxMzgxZDIwZjUyMWEzNjQxZjIzZWI5OWNjYjM0ZTNiYzVkOTYzMzJmZGViYzhlZmE1MGNkYjQxNWU0NTUwMDk1MmNkMWNcIn0iXQ==';
47+
48+
export const VALID_ATTACHMENT_DIDT =
49+
'WyIweGVkMWMwNWRlMTVlMWFkY2Y5ZmEyZWNkNjVjZjg5NWMzYTgzMzQ2OGMwOGFhMmE3YjQ5ZDgyMjFiZWEyMWU1YjgzNDRiNWEwMzAzNmQxMzA5MzQyNTgzMWIxZTFjZGIwZWQ2NTgyMDI4MWU1NzhlMjU5ODJhYzdkYmNkZWJhN2I1MWMiLCJ7XCJpYXRcIjoxNjg4MDYzMTA4LFwiZXh0XCI6MS4wMDAwMDAwMDAwMDE2ODgxZSsyMSxcImlzc1wiOlwiZGlkOmV0aHI6MHhhMWI0YzA5NDI2NDdlNzkwY0ZEMmEwNUE1RkQyNkMwMmM0MjEzOWFlXCIsXCJzdWJcIjpcIjhaTUJnOXNwMFgwQ0FNanhzcVFaOGRzRTJwNVlZWm9lYkRPeWNPUFNNbDA9XCIsXCJhdWRcIjpcIjN3X216VmktaDNtUzc3cFZ4b19ydlJhWjR2WXpOZ0Vudm05ZGcwWnkzYzg9XCIsXCJuYmZcIjoxNjg4MDYzMTA4LFwidGlkXCI6XCJjM2U5ZWRiYy04MDU2LTQ3NGItOGFkMy1hOGI2MzM3NThlOTRcIixcImFkZFwiOlwiMHgzZGExZTM3MmU1ZWU5MjI4YzdlYjBkNmQwZDE2MTAxZjBkNjE5MDY0ODVhYjgzNDMzNWI3Y2YxOGE5ZDNmZWEzNjRmYzFjMTFiNzRlYzBhNTQ0ZTkzNmJkNjQ1Y2U3ZDdkZTIyMTRlNTJlYjZhOThjZTIyNzI1OTEwNDg0ZjJkOTFjXCJ9Il0';

test/lib/factories.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { API_FULL_URL, API_KEY } from './constants';
22
import { MagicAdminSDK } from '../../src/core/sdk';
33

4-
export function createMagicAdminSDK(endpoint = API_FULL_URL) {
5-
return new MagicAdminSDK(API_KEY, { endpoint });
4+
export function createMagicAdminSDK(endpoint = API_FULL_URL, clientId = null) {
5+
return new MagicAdminSDK(API_KEY, { endpoint, clientId });
66
}

test/spec/core/sdk-exceptions/error-factories.spec.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
createServiceError,
99
createExpectedBearerStringError,
1010
createTokenCannotBeUsedYetError,
11+
createAudienceMismatchError,
1112
} from '../../../../src/core/sdk-exceptions';
1213

1314
function errorAssertions(
@@ -55,7 +56,7 @@ test('Creates `ERROR_SECRET_API_KEY_MISSING` error', async () => {
5556
errorAssertions(
5657
error,
5758
'ERROR_SECRET_API_KEY_MISSING',
58-
'Please provide a secret Fortmatic API key that you acquired from the developer dashboard.',
59+
'Please provide a secret Magic API key that you acquired from the developer dashboard.',
5960
);
6061
});
6162

@@ -82,3 +83,12 @@ test('Creates `EXPECTED_BEARER_STRING` error', async () => {
8283
const error = createExpectedBearerStringError();
8384
errorAssertions(error, 'EXPECTED_BEARER_STRING', 'Expected argument to be a string in the `Bearer {token}` format.');
8485
});
86+
87+
test('Creates `AUDIENCE_MISMATCH` error', async () => {
88+
const error = createAudienceMismatchError();
89+
errorAssertions(
90+
error,
91+
'ERROR_AUDIENCE_MISMATCH',
92+
'Audience does not match client ID. Please ensure your secret key matches the application which generated the DID token.',
93+
);
94+
});

test/spec/core/sdk/constructor.spec.ts

+61
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { API_FULL_URL, API_KEY } from '../../../lib/constants';
44
import { TokenModule } from '../../../../src/modules/token';
55
import { UsersModule } from '../../../../src/modules/users';
66
import { UtilsModule } from '../../../../src/modules/utils';
7+
import { get } from '../../../../src/utils/rest';
8+
import { createApiKeyMissingError } from '../../../../src/core/sdk-exceptions';
79

810
test('Initialize `MagicAdminSDK`', () => {
911
const magic = new Magic(API_KEY);
@@ -33,3 +35,62 @@ test('Strips trailing slash(es) from custom endpoint argument', () => {
3335
expect(magicB.apiBaseUrl).toBe('https://example.com');
3436
expect(magicC.apiBaseUrl).toBe('https://example.com');
3537
});
38+
39+
test('Initialize `MagicAdminSDK` using static init and empty options', async () => {
40+
const successRes = Promise.resolve({
41+
client_id: 'foo',
42+
app_scope: 'GLOBAL',
43+
});
44+
(get as any) = jest.fn().mockImplementation(() => successRes);
45+
46+
const magic = await Magic.init(API_KEY, {});
47+
48+
expect(magic.secretApiKey).toBe(API_KEY);
49+
expect(magic.apiBaseUrl).toBe(API_FULL_URL);
50+
expect(magic.token instanceof TokenModule).toBe(true);
51+
expect(magic.users instanceof UsersModule).toBe(true);
52+
});
53+
54+
test('Initialize `MagicAdminSDK` using static init and undefined options', async () => {
55+
const successRes = Promise.resolve({
56+
client_id: 'foo',
57+
app_scope: 'GLOBAL',
58+
});
59+
(get as any) = jest.fn().mockImplementation(() => successRes);
60+
61+
const magic = await Magic.init(API_KEY);
62+
63+
expect(magic.secretApiKey).toBe(API_KEY);
64+
expect(magic.apiBaseUrl).toBe(API_FULL_URL);
65+
expect(magic.token instanceof TokenModule).toBe(true);
66+
expect(magic.users instanceof UsersModule).toBe(true);
67+
});
68+
69+
test('Initialize `MagicAdminSDK` using static init and client ID', async () => {
70+
const magic = await Magic.init(API_KEY, { clientId: '1234' });
71+
72+
expect(magic.secretApiKey).toBe(API_KEY);
73+
expect(magic.apiBaseUrl).toBe(API_FULL_URL);
74+
expect(magic.token instanceof TokenModule).toBe(true);
75+
expect(magic.users instanceof UsersModule).toBe(true);
76+
});
77+
78+
test('Initialize `MagicAdminSDK` using static init and endpoint', async () => {
79+
const successRes = Promise.resolve({
80+
client_id: 'foo',
81+
app_scope: 'GLOBAL',
82+
});
83+
(get as any) = jest.fn().mockImplementation(() => successRes);
84+
85+
const magic = await Magic.init(API_KEY, { endpoint: 'https://example.com' });
86+
87+
expect(magic.secretApiKey).toBe(API_KEY);
88+
expect(magic.apiBaseUrl).toBe('https://example.com');
89+
expect(magic.token instanceof TokenModule).toBe(true);
90+
expect(magic.users instanceof UsersModule).toBe(true);
91+
});
92+
93+
test('Initialize `MagicAdminSDK` missing API Key', async () => {
94+
const expectedError = createApiKeyMissingError();
95+
expect(Magic.init(null, { clientId: '1234' })).rejects.toThrow(expectedError);
96+
});

test/spec/modules/token/validate.spec.ts

+18
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,32 @@ import {
66
EXPIRED_DIDT,
77
INVALID_DIDT_MALFORMED_CLAIM,
88
VALID_FUTURE_MARKED_DIDT,
9+
VALID_ATTACHMENT_DIDT,
910
} from '../../../lib/constants';
1011
import {
1112
createIncorrectSignerAddressError,
1213
createTokenExpiredError,
1314
createFailedRecoveringProofError,
1415
createMalformedTokenError,
1516
createTokenCannotBeUsedYetError,
17+
createAudienceMismatchError,
1618
} from '../../../../src/core/sdk-exceptions';
1719

1820
test('Successfully validates DIDT', async () => {
21+
const sdk = createMagicAdminSDK(undefined, 'did:magic:f54168e9-9ce9-47f2-81c8-7cb2a96b26ba');
22+
expect(() => sdk.token.validate(VALID_DIDT)).not.toThrow();
23+
});
24+
25+
test('Successfully validates DIDT without checking audience', async () => {
1926
const sdk = createMagicAdminSDK();
2027
expect(() => sdk.token.validate(VALID_DIDT)).not.toThrow();
2128
});
2229

30+
test('Successfully validates DIDT with attachment', async () => {
31+
const sdk = createMagicAdminSDK();
32+
expect(() => sdk.token.validate(VALID_ATTACHMENT_DIDT, '[email protected]')).not.toThrow();
33+
});
34+
2335
test('Fails when signer address mismatches signature', async () => {
2436
const sdk = createMagicAdminSDK();
2537
const expectedError = createIncorrectSignerAddressError();
@@ -49,3 +61,9 @@ test('Fails if decoding token fails', async () => {
4961
const expectedError = createMalformedTokenError();
5062
expect(() => sdk.token.validate(INVALID_DIDT_MALFORMED_CLAIM)).toThrow(expectedError);
5163
});
64+
65+
test('Fails if aud is incorrect', async () => {
66+
const sdk = createMagicAdminSDK(undefined, 'different');
67+
const expectedError = createAudienceMismatchError();
68+
expect(() => sdk.token.validate(VALID_DIDT)).toThrow(expectedError);
69+
});

0 commit comments

Comments
 (0)