@atproto/jwk-jose
Version:
`jose` based implementation of @atproto/jwk Key's
171 lines • 6.77 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.JoseKey = void 0;
const jose_1 = require("jose");
const jwk_1 = require("@atproto/jwk");
const util_js_1 = require("./util.js");
const { JOSEError } = jose_1.errors;
class JoseKey extends jwk_1.Key {
/**
* Some runtimes (e.g. Bun) require an `alg` second argument to be set when
* invoking `importJWK`. In order to be compatible with these runtimes, we
* provide the following method to ensure the `alg` is always set. We also
* take the opportunity to ensure that the `alg` is compatible with this key.
*/
async getKeyObj(alg) {
if (!this.algorithms.includes(alg)) {
throw new jwk_1.JwkError(`Key cannot be used with algorithm "${alg}"`);
}
try {
return await (0, jose_1.importJWK)(this.jwk, alg);
}
catch (cause) {
throw new jwk_1.JwkError('Failed to import JWK', undefined, { cause });
}
}
async createJwt(header, payload) {
try {
const { kid } = header;
if (kid && kid !== this.kid) {
throw new jwk_1.JwtCreateError(`Invalid "kid" (${kid}) used to sign with key "${this.kid}"`);
}
const { alg } = header;
if (!alg) {
throw new jwk_1.JwtCreateError('Missing "alg" in JWT header');
}
const keyObj = await this.getKeyObj(alg);
const jwtBuilder = new jose_1.SignJWT(payload).setProtectedHeader({
...header,
alg,
kid: this.kid,
});
const signedJwt = await jwtBuilder.sign(keyObj);
return signedJwt;
}
catch (cause) {
if (cause instanceof JOSEError) {
throw new jwk_1.JwtCreateError(cause.message, cause.code, { cause });
}
else {
throw jwk_1.JwtCreateError.from(cause);
}
}
}
async verifyJwt(token, options) {
try {
const result = await (0, jose_1.jwtVerify)(token, async ({ alg }) => this.getKeyObj(alg), { ...options, algorithms: this.algorithms });
// @NOTE if all tokens are signed exclusively through createJwt(), then
// there should be no need to parse the payload and headers here. But
// since the JWT could have been signed with the same key from somewhere
// else, let's parse it to ensure the integrity (and type safety) of the
// data.
const headerParsed = jwk_1.jwtHeaderSchema.safeParse(result.protectedHeader);
if (!headerParsed.success) {
throw new jwk_1.JwtVerifyError('Invalid JWT header', undefined, {
cause: headerParsed.error,
});
}
const payloadParsed = jwk_1.jwtPayloadSchema.safeParse(result.payload);
if (!payloadParsed.success) {
throw new jwk_1.JwtVerifyError('Invalid JWT payload', undefined, {
cause: payloadParsed.error,
});
}
return {
protectedHeader: headerParsed.data,
// "requiredClaims" enforced by jwtVerify()
payload: payloadParsed.data,
};
}
catch (cause) {
if (cause instanceof JOSEError) {
throw new jwk_1.JwtVerifyError(cause.message, cause.code, { cause });
}
else {
throw jwk_1.JwtVerifyError.from(cause);
}
}
}
static async generateKeyPair(allowedAlgos = ['ES256'], options) {
if (!allowedAlgos.length) {
throw new jwk_1.JwkError('No algorithms provided for key generation');
}
const errors = [];
for (const alg of allowedAlgos) {
try {
return await (0, jose_1.generateKeyPair)(alg, options);
}
catch (err) {
errors.push(err);
}
}
throw new jwk_1.JwkError('Failed to generate key pair', undefined, {
cause: new AggregateError(errors, 'None of the algorithms worked'),
});
}
static async generate(allowedAlgos = ['ES256'], kid, options) {
const kp = await this.generateKeyPair(allowedAlgos, {
...options,
extractable: true,
});
return this.fromKeyLike(kp.privateKey, kid);
}
static async fromImportable(input, kid) {
if (typeof input === 'string') {
// PKCS8
if (input.startsWith('-----')) {
// The "alg" is only needed in WebCrypto (NodeJS will be fine)
return this.fromPKCS8(input, '', kid);
}
// Jwk (string)
if (input.startsWith('{')) {
return this.fromJWK(input, kid);
}
throw new jwk_1.JwkError('Invalid input');
}
if (typeof input === 'object') {
// Jwk
if ('kty' in input || 'alg' in input) {
return this.fromJWK(input, kid);
}
// KeyLike
return this.fromKeyLike(input, kid);
}
throw new jwk_1.JwkError('Invalid input');
}
/**
* @see {@link exportJWK}
*/
static async fromKeyLike(keyLike, kid, alg) {
const jwk = await (0, jose_1.exportJWK)(keyLike);
if (alg) {
if (!jwk.alg)
jwk.alg = alg;
else if (jwk.alg !== alg)
throw new jwk_1.JwkError('Invalid "alg" in JWK');
}
return this.fromJWK(jwk, kid);
}
/**
* @see {@link importPKCS8}
*/
static async fromPKCS8(pem, alg, kid) {
const keyLike = await (0, jose_1.importPKCS8)(pem, alg, { extractable: true });
return this.fromKeyLike(keyLike, kid);
}
static async fromJWK(input, inputKid) {
const jwk = typeof input === 'string' ? JSON.parse(input) : input;
if (!jwk || typeof jwk !== 'object')
throw new jwk_1.JwkError('Invalid JWK');
const kid = (0, util_js_1.either)(jwk.kid, inputKid);
// Backwards compatibility with old behavior
if (jwk.use != null && (0, jwk_1.isPrivateJwk)(jwk)) {
console.warn('Deprecation warning: Private JWK with a "use" property will be rejected in the future. Please remove replace "use" with (valid) "key_ops".');
jwk.key_ops ??= jwk.use === 'sig' ? ['sign'] : ['encrypt'];
delete jwk.use;
}
return new JoseKey(jwk_1.jwkSchema.parse({ ...jwk, kid }));
}
}
exports.JoseKey = JoseKey;
//# sourceMappingURL=jose-key.js.map