diff --git a/LICENSE b/LICENSE index 6877c27..6fdccc4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 Virgil Security +Copyright 2015-2019 Virgil Security Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/package.json b/package.json index e516340..24d8fc3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "virgil-sdk", - "version": "5.2.1", + "version": "5.2.2", "description": "Virgil Security Services SDK", "contributors": [ "Eugene Baranov (https://github.com/ebaranov/)", diff --git a/src/Auth/JwtGenerator.ts b/src/Auth/JwtGenerator.ts index a89d554..f892019 100644 --- a/src/Auth/JwtGenerator.ts +++ b/src/Auth/JwtGenerator.ts @@ -57,29 +57,29 @@ export class JwtGenerator { /** * @see {@link IJwtGeneratorOptions.appId} */ - public readonly appId: string; + readonly appId: string; /** * @see {@link IJwtGeneratorOptions.apiKey} */ - public readonly apiKey: IPrivateKey; + readonly apiKey: IPrivateKey; /** * @see {@link IJwtGeneratorOptions.apiKeyId} */ - public readonly apiKeyId: string; + readonly apiKeyId: string; /** * @see {@link IJwtGeneratorOptions.accessTokenSigner} */ - public readonly accessTokenSigner: IAccessTokenSigner; + readonly accessTokenSigner: IAccessTokenSigner; /** * @see {@link IJwtGeneratorOptions.millisecondsToLive} */ - public readonly millisecondsToLive: number; + readonly millisecondsToLive: number; - public constructor (options: IJwtGeneratorOptions) { + constructor (options: IJwtGeneratorOptions) { validateOptions(options); this.appId = options.appId; @@ -99,7 +99,13 @@ export class JwtGenerator { * @param {IExtraData} ada - Additional data to be encoded in the JWT. * @returns {Jwt} */ - public generateToken(identity: string, ada?: IExtraData) { + generateToken(identity: string, ada?: IExtraData) { + if (!identity) { + throw new TypeError( + 'Illegal arguments for function `generateToken`. Argument `identity` is required.' + ); + } + const iat = getUnixTimestamp(new Date()); const exp = getUnixTimestamp(new Date().getTime() + this.millisecondsToLive); diff --git a/src/__tests__/index.ts b/src/__tests__/index.ts index 18cd2c0..dab98dc 100644 --- a/src/__tests__/index.ts +++ b/src/__tests__/index.ts @@ -7,6 +7,7 @@ import './unit/StorageAdapter.test'; import './unit/KeyStorage.test'; import './unit/KeyEntryStorage.test'; import './unit/PrivateKeyStorage.test'; +import './unit/JwtGenerator.test'; import './integration/CardManager.test'; import './integration/CardVerifier.test'; import './integration/Jwt.test'; diff --git a/src/__tests__/integration/CardManager.test.ts b/src/__tests__/integration/CardManager.test.ts index a28abf5..f1ba957 100644 --- a/src/__tests__/integration/CardManager.test.ts +++ b/src/__tests__/integration/CardManager.test.ts @@ -13,6 +13,9 @@ import { ICard } from '../../Cards/ICard'; import { compatData } from './data'; +const WELL_KNOWN_IDENTITY = `js_sdk_well_known_identity${Date.now()}@virgil.com`; +let WELL_KNOWN_CARD_ID:string; + const init = () => { const crypto = new VirgilCrypto(); const accessTokenSigner = new VirgilAccessTokenSigner(crypto); @@ -36,7 +39,7 @@ const init = () => { millisecondsToLive: 1000 }); - const accessTokenProvider = new GeneratorJwtProvider(jwtGenerator); + const accessTokenProvider = new GeneratorJwtProvider(jwtGenerator, undefined, WELL_KNOWN_IDENTITY); const cardVerifier = new VirgilCardVerifier(cardCrypto); @@ -56,9 +59,6 @@ const init = () => { }; }; -const WELL_KNOWN_IDENTITY = `js_sdk_well_known_identity${Date.now()}@virgil.com`; -let WELL_KNOWN_CARD_ID:string; - describe('CardManager', function () { this.timeout(10000); @@ -460,10 +460,10 @@ describe('CardManager', function () { const getTokenFn = (context: ITokenContext) => { if (context.forceReload) { // generating good token - return Promise.resolve(fixture.jwtGenerator.generateToken(context.identity!)); + return Promise.resolve(fixture.jwtGenerator.generateToken(WELL_KNOWN_IDENTITY)); } else { // generating expired token - const expiredToken = fixture.expiredTokenGenerator.generateToken(context.identity!); + const expiredToken = fixture.expiredTokenGenerator.generateToken(WELL_KNOWN_IDENTITY); const sleep = new Promise(resolve => setTimeout(resolve, 2000)); // sleeping is needed because minimum token lifetime is 1 second return sleep.then(() => expiredToken); diff --git a/src/__tests__/unit/JwtGenerator.test.ts b/src/__tests__/unit/JwtGenerator.test.ts new file mode 100644 index 0000000..8bbc929 --- /dev/null +++ b/src/__tests__/unit/JwtGenerator.test.ts @@ -0,0 +1,137 @@ +/// + +import { VirgilCrypto, VirgilAccessTokenSigner } from 'virgil-crypto'; +import { JwtGenerator } from '../../Auth/JwtGenerator'; +import { JwtContentType, VirgilContentType, IssuerPrefix, SubjectPrefix } from '../../Auth/jwt-constants'; +import { getUnixTimestamp } from '../../Lib/timestamp'; + +const virgilCrypto = new VirgilCrypto(); +const accessTokenSigner = new VirgilAccessTokenSigner(virgilCrypto); + +describe('JwtGenerator', () => { + describe('constructor', () => { + it('throws when options are not provided', () => { + assert.throws(() => { + new JwtGenerator(null as any); + }, 'JwtGenerator options must be provided'); + }); + + it('throws when API Key ID is not provided', () => { + assert.throws(() => { + new JwtGenerator({ + apiKey: virgilCrypto.generateKeys().privateKey, + appId: 'irrelevant', + accessTokenSigner + } as any); + }, '`apiKeyId` is required'); + }); + + it('throws when API Key is not provided', () => { + assert.throws(() => { + new JwtGenerator({ + apiKeyId: 'irrelevant', + appId: 'irrelevant', + accessTokenSigner + } as any); + }, '`apiKey` is required'); + }); + + it('throws when App ID is not provided', () => { + assert.throws(() => { + new JwtGenerator({ + apiKey: virgilCrypto.generateKeys().privateKey, + apiKeyId: 'irrelevant', + accessTokenSigner + } as any); + }, '`appId` is required'); + }); + + it('throws when accessTokenSigner is not provided', () => { + assert.throws(() => { + new JwtGenerator({ + apiKey: virgilCrypto.generateKeys().privateKey, + apiKeyId: 'irrelevant', + appId: 'irrelevant' + } as any); + }, '`accessTokenSigner` is required'); + }); + + it('uses "millisecondsToLive" value provided in options', () => { + const expectedMillisecondsToLive = 99999; + const generator = new JwtGenerator({ + millisecondsToLive: expectedMillisecondsToLive, + apiKey: virgilCrypto.generateKeys().privateKey, + apiKeyId: 'irrelevant', + appId: 'irrelevant', + accessTokenSigner + }); + + assert.equal(generator.millisecondsToLive, expectedMillisecondsToLive); + }); + + it('uses 20 minutes by default for "millisecondsToLive"', () => { + const expectedMillisecondsToLive = 20 * 60 * 1000; + const generator = new JwtGenerator({ + apiKey: virgilCrypto.generateKeys().privateKey, + apiKeyId: 'irrelevant', + appId: 'irrelevant', + accessTokenSigner + }); + + assert.equal(generator.millisecondsToLive, expectedMillisecondsToLive); + }); + }); + + describe('generateToken', () => { + it('throws when identity is not provided', () => { + const generator = new JwtGenerator({ + apiKey: virgilCrypto.generateKeys().privateKey, + apiKeyId: 'irrelevant', + appId: 'irrelevant', + accessTokenSigner + }); + + assert.throws(() => { + generator.generateToken(undefined as any); + }, '`identity` is required'); + }); + + it('generates a valid JWT', () => { + const keypair = virgilCrypto.generateKeys(); + const generator = new JwtGenerator({ + apiKey: keypair.privateKey, + apiKeyId: keypair.privateKey.identifier.toString('base64'), + appId: 'my_app_id', + accessTokenSigner + }); + + const expectedIdentity = 'test_identity'; + const expectedAda = { additional: 'data' }; + const token = generator.generateToken(expectedIdentity, expectedAda); + + assert.isDefined(token.header, 'JWT has header'); + assert.isDefined(token.body, 'JWT has body'); + assert.isDefined(token.signature, 'JWT has signature'); + + assert.equal(token.header.alg, accessTokenSigner.getAlgorithm(), 'algorithm is correct'); + assert.equal(token.header.kid, keypair.privateKey.identifier.toString('base64'), 'key id is correct'); + assert.equal(token.header.typ, JwtContentType, 'token type is correct'); + assert.equal(token.header.cty, VirgilContentType, 'content type is correct'); + + assert.equal(token.body.iss, IssuerPrefix + 'my_app_id', 'issuer is correct'); + assert.equal(token.body.sub, SubjectPrefix + 'test_identity', 'subject is correct'); + assert.equal(token.body.iat, getUnixTimestamp(Date.now()), 'issued at is correct'); + assert.equal( + token.body.exp, + getUnixTimestamp(Date.now() + generator.millisecondsToLive), + 'expires at is correct' + ); + assert.deepEqual(token.body.ada, expectedAda, 'additional data is correct'); + + assert.isTrue( + virgilCrypto.verifySignature(token.unsignedData, token.signature!, keypair.publicKey), + 'signature is correct' + ); + }); + }); +});