Skip to content

Commit 1720c53

Browse files
authored
Merge pull request #111 from magiclabs/ravi-sc80094-ValidateAud
Validate 'aud' in DID Token
2 parents 0ae68b8 + bc1e9b6 commit 1720c53

File tree

12 files changed

+241
-11
lines changed

12 files changed

+241
-11
lines changed

CHANGELOG.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,78 @@
1+
# v2.0.0 (July 10, 2023)
2+
3+
## Summary
4+
- 🚀 **Added:** Magic Connect developers can now use the Admin SDK to validate DID tokens. [#111](https://github.com/magiclabs/magic-admin-js/pull/111) ([@magic-ravi](https://github.com/magic-ravi))
5+
- ⚠️ **Changed:** After creating the Magic instance, it is now necessary to call a new initialize method for Magic Connect developers that want to utilize the Admin SDK. [#111](https://github.com/magiclabs/magic-admin-js/pull/111) ([@magic-ravi](https://github.com/magic-ravi))
6+
- 🛡️ **Security:** Additional validation of `aud` (client ID) is now being done during initialization of the SDK. [#111](https://github.com/magiclabs/magic-admin-js/pull/111) ([@magic-ravi](https://github.com/magic-ravi))
7+
8+
## Developer Notes
9+
10+
### 🚀 Added
11+
12+
#### Admin SDK for MC
13+
Magic Connect developers can now use the Admin SDK to validate DID tokens.
14+
15+
**Details**
16+
There is full support for all `TokenResource` SDK methods for MC. This is intended to be used with client side `magic-js` SDK which will now emit an `id-token-created` event with a DID token upon login via the `connectWithUI` method.
17+
18+
This functionality is replicated on our other SDKs on Python and Ruby.
19+
20+
### ⚠️ Changed
21+
22+
#### Constructor initialization
23+
24+
The existing constructor has been deprecated in place of a new async `init` method.
25+
The `init` method will pull clientId from Magic servers if one is not provided in the `options` parameter.
26+
27+
**Previous Version**
28+
```javascript
29+
const magic = new Magic(secretKey);
30+
try {
31+
magic.token.validate(DIDT);
32+
} catch (e) {
33+
console.log(e);
34+
}
35+
try {
36+
await magic.users.getMetadataByToken(DIDT);
37+
} catch (e) {
38+
console.log(e);
39+
}
40+
```
41+
42+
**Current Version**
43+
```javascript
44+
const magic = await Magic.init(mcSecretKey);
45+
try {
46+
magic.token.validate(DIDT);
47+
} catch (e) {
48+
console.log(e);
49+
}
50+
try {
51+
await magic.users.getMetadataByToken(DIDT);
52+
} catch (e) {
53+
console.log(e);
54+
}
55+
```
56+
57+
#### Attachment Validation
58+
59+
- Skip validation of attachment if 'none' is passed in `validate`.
60+
61+
### 🛡️ Security
62+
63+
#### Client ID Validation
64+
65+
Additional validation of `aud` (client ID) is now being done during initialization of the SDK. This is for both Magic Connect and Magic Auth developers.
66+
67+
68+
### 🚨 Breaking
69+
70+
None, all changes are fully backwards compatiable.
71+
72+
### Authors: 1
73+
74+
- Ravi Bhankharia ([@magic-ravi](https://github.com/magic-ravi))
75+
176
# v1.10.1 (Fri Jul 07 2023)
277

378
#### 🐛 Bug Fix

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,22 @@ 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-
37-
// Read the docs to learn about next steps! 🚀
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) => {
39+
magic
40+
});
41+
// Validate a token
42+
try {
43+
magic.token.validate("DIDToken");
44+
} catch (e) {
45+
console.log(e);
46+
}
47+
// Magic Auth - Get User Email
48+
try {
49+
await magic.users.getMetadataByToken("DIDToken");
50+
} catch (e) {
51+
console.log(e);
52+
}
3853
```

src/core/sdk-exceptions.ts

Lines changed: 8 additions & 1 deletion
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

Lines changed: 32 additions & 1 deletion
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

Lines changed: 11 additions & 3 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 11 additions & 1 deletion
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+
});

0 commit comments

Comments
 (0)