atlassian-connect-auth
Version:
Helper for handling webhooks from Atlassian products
189 lines (188 loc) • 7.46 kB
JavaScript
;
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;
}