Files
speckle-server/packages/server/modules/gatekeeper/tests/unit/validateLicense.spec.ts
T
Gergő Jedlicska 8cba7eb6f7 gergo/web 1968 add features list (#3332)
* feat(gatekeeper): add gatekeeper module feature flag

* feat(gatekeeper): add workspace pricing table domain

* feat(gatekeeper): add checkout session creation

* feat(gatekeeper): verify stripe signature

* wip(gatekeeper): checkout callbacks

* feat(gatekeeper): add unlimited and academia plan types

* refactor(envHelper): getStringFromEnv helper

* chore(gatekeeper): add future todos

* feat(gatekeeper): add productId to the subscription domain

* feat(gatekeeper): add in memory repositories

* feat(gatekeeper): add more errors

* feat(gatekeeper): complete checkout session service

* feat(gatekeeper): add stripe client implementation

* feat(gatekeeper): add checkout session completion webhook callback path

* feat(gendo): fix not needing env vars if gendo module is not enabled

* feat(gatekeeper): require a license for billing

* chore(gatekeeper): cleanup before testing

* feat(gatekeeper): subscriptionData parsing model

* ci: add billing integration and gatekeeper modules to test config

* test(gatekeeper): add checkout service tests

* feat(gatekeeper): make completeCheckout callback idempotent properly

* feat(gatekeeper): move to knex based repositories

* test(gatekeeper): billing repository tests

* feat(gatekeeper): add yearly billing cycle toggle

* feat(ci): add stripe integration context to test job

* feat(billingPage): conditionally render the checkout CTAs

* fix(gatekeeper): remove flaky test condition

* feat(helm): add billing integration feature flag
2024-10-20 15:40:31 +02:00

195 lines
5.3 KiB
TypeScript

import type { LicenseTokenClaims } from '@/modules/gatekeeper/domain/types'
import { validateLicenseModuleAccess } from '@/modules/gatekeeper/services/validateLicense'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
import * as jose from 'jose'
describe('validateLicense @gatekeeper', () => {
describe('validateLicenseModuleAccess', () => {
it('fails is the token is giberish', async () => {
const alg = 'RS256'
const { publicKey } = await jose.generateKeyPair(alg)
const result = await validateLicenseModuleAccess({
licenseToken: cryptoRandomString({ length: 32 }),
canonicalUrl: 'https://example.com',
publicKey,
requiredModules: ['workspaces']
})
expect(result).to.be.false
})
it('fails if the token is signed by another private key', async () => {
const canonicalUrl = 'https://example.com'
const alg = 'RS256'
const { publicKey } = await jose.generateKeyPair(alg)
const claims: LicenseTokenClaims = {
allowedDomains: [canonicalUrl],
enabledModules: {
workspaces: true
}
}
const { privateKey } = await jose.generateKeyPair(alg)
const licenseToken = await new jose.SignJWT(claims)
.setProtectedHeader({ alg })
.setIssuedAt()
.sign(privateKey)
const result = await validateLicenseModuleAccess({
licenseToken,
canonicalUrl,
publicKey,
requiredModules: ['workspaces']
})
expect(result).to.be.false
})
it('fails if the token is not in the correct payload format', async () => {
const canonicalUrl = 'https://example.com'
const alg = 'RS256'
const { privateKey, publicKey } = await jose.generateKeyPair(alg)
const claims = {
enabledModules: {
workspaces: true
}
}
const licenseToken = await new jose.SignJWT(claims)
.setProtectedHeader({ alg })
.setIssuedAt()
.sign(privateKey)
const result = await validateLicenseModuleAccess({
licenseToken,
canonicalUrl,
publicKey,
requiredModules: ['workspaces']
})
expect(result).to.be.false
})
it('fails if the token domain claim does not include the canonicalUrl', async () => {
const canonicalUrl = 'https://example.com'
const alg = 'RS256'
const { privateKey, publicKey } = await jose.generateKeyPair(alg)
const claims: LicenseTokenClaims = {
allowedDomains: ['https://not.allowed'],
enabledModules: {
workspaces: true
}
}
const licenseToken = await new jose.SignJWT(claims)
.setProtectedHeader({ alg })
.setIssuedAt()
.sign(privateKey)
const result = await validateLicenseModuleAccess({
licenseToken,
canonicalUrl,
publicKey,
requiredModules: ['workspaces']
})
expect(result).to.be.false
})
it('fails if the token module claims do not enable all the required modules', async () => {
const canonicalUrl = 'https://example.com'
const alg = 'RS256'
const { privateKey, publicKey } = await jose.generateKeyPair(alg)
const claims: LicenseTokenClaims = {
allowedDomains: [canonicalUrl],
enabledModules: {
workspaces: false
}
}
const licenseToken = await new jose.SignJWT(claims)
.setProtectedHeader({ alg })
.setIssuedAt()
.sign(privateKey)
const result = await validateLicenseModuleAccess({
licenseToken,
canonicalUrl,
publicKey,
requiredModules: ['workspaces', 'gatekeeper']
})
expect(result).to.be.false
})
it('fails if the token string is tampered with', async () => {
const canonicalUrl = 'https://example.com'
const alg = 'RS256'
const { privateKey, publicKey } = await jose.generateKeyPair(alg)
const claims: LicenseTokenClaims = {
allowedDomains: [canonicalUrl],
enabledModules: {
workspaces: true
}
}
const licenseToken = await new jose.SignJWT(claims)
.setProtectedHeader({ alg })
.setIssuedAt()
.sign(privateKey)
const hackedToken = licenseToken
.split('.')
.map((value, index) => {
if (index === 1) return `${value}hack`
return value
})
.join('.')
const result = await validateLicenseModuleAccess({
licenseToken: hackedToken,
canonicalUrl,
publicKey,
requiredModules: ['workspaces']
})
expect(result).to.be.false
})
it('succeeds for valid tokens', async () => {
const canonicalUrl = 'https://example.com'
const alg = 'RS256'
const { privateKey, publicKey } = await jose.generateKeyPair(alg)
const claims: LicenseTokenClaims = {
allowedDomains: [canonicalUrl],
enabledModules: {
workspaces: true
}
}
const licenseToken = await new jose.SignJWT(claims)
.setProtectedHeader({ alg })
.setIssuedAt()
.sign(privateKey)
const result = await validateLicenseModuleAccess({
licenseToken,
canonicalUrl,
publicKey,
requiredModules: ['workspaces']
})
expect(result).to.be.true
})
})
})