UNPKG

atlassian-connect-auth

Version:
189 lines (188 loc) 7.46 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.verifyRequest = exports.verifyInstallation = void 0; const AuthError_1 = require("./AuthError"); const Jwt_1 = require("./Jwt"); const QueryStringHash_1 = require("./QueryStringHash"); const types_1 = require("./types"); /** * Verifies a Connect request installation. * Use this function to make sure the request is valid before persisting any data. * This function handles both new installations and re-installations or installation updates. */ async function verifyInstallation({ asymmetricKeyProvider, authDataProvider, authorizationMethod = 'any', baseUrl, credentialsLoader, queryStringHashType, }) { const clientKey = authDataProvider.extractClientKey(); const rawConnectJwt = authDataProvider.extractConnectJwt(); // Parse unverified JWT let unverifiedConnectJwt; if (rawConnectJwt) { unverifiedConnectJwt = (0, Jwt_1.decodeUnverifiedConnectJwt)(rawConnectJwt); } // Check for a signed installation if (authorizationMethod === 'publicKey' || (authorizationMethod === 'any' && (0, Jwt_1.isAsymmetricAlgorithm)(unverifiedConnectJwt?.alg))) { const connectJwt = await verifyAsymmetricallySignedRequest({ authDataProvider, asymmetricKeyProvider, baseUrl, queryStringHashType, unverifiedConnectJwt, }); // New installation const credentials = await credentialsLoader(clientKey); if (!credentials) { return { type: types_1.InstallationType.newInstallation, connectJwt, clientKey, }; } // Installation update return { type: types_1.InstallationType.update, clientKey, connectJwt, storedEntity: credentials.storedEntity, }; } // Fallback to unsigned installation // In non-authenticated installs, we only check issuer if there's a JWT if (unverifiedConnectJwt && unverifiedConnectJwt.iss !== clientKey) { throw new AuthError_1.AuthError('Wrong issuer', { code: AuthError_1.AuthErrorCode.WRONG_ISSUER, unverifiedConnectJwt, }); } // Unsigned new installation const credentials = await credentialsLoader(clientKey); if (!credentials) { return { type: types_1.InstallationType.newInstallation, clientKey, }; } const { sharedSecret, storedEntity } = credentials; // Verify installation update if (rawConnectJwt) { const connectJwt = (0, Jwt_1.verifyConnectJwt)({ rawConnectJwt, sharedSecret, unverifiedConnectJwt, }); (0, QueryStringHash_1.verifyQueryStringHash)({ queryStringHashType, connectJwt, computeQueryStringHashFunction: () => authDataProvider.computeQueryStringHash(baseUrl), }); return { type: types_1.InstallationType.update, clientKey, connectJwt, storedEntity, }; } throw new AuthError_1.AuthError('Unauthorized update request', { code: AuthError_1.AuthErrorCode.UNAUTHORIZED_REQUEST, }); } exports.verifyInstallation = verifyInstallation; /** * Verifies any post-installation incoming connect requests using currently stored Shared Secret. * Use this function to verify the request was actually initiated by Atlassian Connect service and that its content * is not tainted via the Query String Hash algorithm. * This function handles API, frame-loading, context, and some app-lifecycle requests. */ async function verifyRequest({ asymmetricKeyProvider, authDataProvider, authorizationMethod = 'any', baseUrl, credentialsLoader, queryStringHashType, }) { const rawConnectJwt = authDataProvider.extractConnectJwt(); if (!rawConnectJwt) { throw new AuthError_1.AuthError('Missing JWT', { code: AuthError_1.AuthErrorCode.MISSING_JWT }); } // Load existing installation const unverifiedConnectJwt = (0, Jwt_1.decodeUnverifiedConnectJwt)(rawConnectJwt); const credentials = await credentialsLoader(unverifiedConnectJwt.iss); if (!credentials) { throw new AuthError_1.AuthError('Unknown issuer', { code: AuthError_1.AuthErrorCode.UNKNOWN_ISSUER, unverifiedConnectJwt, }); } // Check for a signed uninstallation if (authorizationMethod === 'publicKey' || (authorizationMethod === 'any' && (0, Jwt_1.isAsymmetricAlgorithm)(unverifiedConnectJwt.alg))) { const connectJwt = await verifyAsymmetricallySignedRequest({ authDataProvider, asymmetricKeyProvider, baseUrl, queryStringHashType, unverifiedConnectJwt, }); return { connectJwt, storedEntity: credentials.storedEntity }; } const connectJwt = (0, Jwt_1.verifyConnectJwt)({ rawConnectJwt, sharedSecret: credentials.sharedSecret, unverifiedConnectJwt, }); (0, QueryStringHash_1.verifyQueryStringHash)({ queryStringHashType, connectJwt, computeQueryStringHashFunction: () => authDataProvider.computeQueryStringHash(baseUrl), }); return { connectJwt, storedEntity: credentials.storedEntity }; } exports.verifyRequest = verifyRequest; /** * Verifies a Connect request containing an asymmetrically signed JWT token. */ async function verifyAsymmetricallySignedRequest({ baseUrl, authDataProvider, asymmetricKeyProvider, queryStringHashType, unverifiedConnectJwt, }) { // Check JWT if (!unverifiedConnectJwt) { throw new AuthError_1.AuthError('Missing JWT', { code: AuthError_1.AuthErrorCode.MISSING_JWT }); } // Check issuer const clientKey = authDataProvider.extractClientKey(); if (unverifiedConnectJwt.iss !== clientKey) { throw new AuthError_1.AuthError('Wrong issuer', { code: AuthError_1.AuthErrorCode.WRONG_ISSUER, unverifiedConnectJwt, }); } // Check audience if (!unverifiedConnectJwt.aud?.includes(baseUrl)) { throw new AuthError_1.AuthError('Wrong audience', { code: AuthError_1.AuthErrorCode.WRONG_AUDIENCE, unverifiedConnectJwt, }); } if (!unverifiedConnectJwt.kid) { throw new AuthError_1.AuthError('Missing token kid', { code: AuthError_1.AuthErrorCode.MISSING_KID, unverifiedConnectJwt, }); } // Fetch public key let publicKey; try { publicKey = await asymmetricKeyProvider.get(unverifiedConnectJwt.kid, unverifiedConnectJwt); } catch (error) { throw new AuthError_1.AuthError('Failed to obtain public key', { code: AuthError_1.AuthErrorCode.FAILED_TO_OBTAIN_PUBLIC_KEY, originError: error, unverifiedConnectJwt, }); } // Verify asymmetric JWT const connectJwt = (0, Jwt_1.verifyAsymmetricConnectJwt)({ rawConnectJwt: authDataProvider.extractConnectJwt(), publicKey, unverifiedConnectJwt, }); // Verify QSH (0, QueryStringHash_1.verifyQueryStringHash)({ queryStringHashType, connectJwt, computeQueryStringHashFunction: () => authDataProvider.computeQueryStringHash(baseUrl), }); return connectJwt; }