UNPKG

atlassian-connect-auth

Version:
288 lines (252 loc) 7.97 kB
import { AuthDataProvider } from './authDataProvider'; import { AuthError, AuthErrorCode } from './AuthError'; import { decodeUnverifiedConnectJwt, isAsymmetricAlgorithm, verifyAsymmetricConnectJwt, verifyConnectJwt, } from './Jwt'; import { KeyProvider } from './publicKeyProvider'; import { verifyQueryStringHash } from './QueryStringHash'; import { ConnectJwt, CredentialsLoader, InstallationQueryStringHashType, InstallationType, QueryStringHashType, } from './types'; interface CommonVerifyArgs<E, Q> { asymmetricKeyProvider: KeyProvider; authDataProvider: AuthDataProvider; authorizationMethod?: 'sharedSecret' | 'publicKey' | 'any'; baseUrl: string; credentialsLoader: CredentialsLoader<E>; queryStringHashType?: Q; } export type VerifyInstallationArgs<E> = CommonVerifyArgs<E, InstallationQueryStringHashType>; export interface NewInstallationResponse { type: InstallationType.newInstallation; clientKey: string; connectJwt?: ConnectJwt; } export interface UpdateInstallationResponse<E> { type: InstallationType.update; clientKey: string; connectJwt: ConnectJwt; storedEntity: E; } export type VerifyInstallationResponse<E> = NewInstallationResponse | UpdateInstallationResponse<E>; /** * 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. */ export async function verifyInstallation<E>({ asymmetricKeyProvider, authDataProvider, authorizationMethod = 'any', baseUrl, credentialsLoader, queryStringHashType, }: VerifyInstallationArgs<E>): Promise<VerifyInstallationResponse<E>> { const clientKey = authDataProvider.extractClientKey(); const rawConnectJwt = authDataProvider.extractConnectJwt(); // Parse unverified JWT let unverifiedConnectJwt; if (rawConnectJwt) { unverifiedConnectJwt = decodeUnverifiedConnectJwt(rawConnectJwt); } // Check for a signed installation if ( authorizationMethod === 'publicKey' || (authorizationMethod === 'any' && isAsymmetricAlgorithm(unverifiedConnectJwt?.alg)) ) { const connectJwt = await verifyAsymmetricallySignedRequest({ authDataProvider, asymmetricKeyProvider, baseUrl, queryStringHashType, unverifiedConnectJwt, }); // New installation const credentials = await credentialsLoader(clientKey); if (!credentials) { return { type: InstallationType.newInstallation, connectJwt, clientKey, }; } // Installation update return { type: 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('Wrong issuer', { code: AuthErrorCode.WRONG_ISSUER, unverifiedConnectJwt, }); } // Unsigned new installation const credentials = await credentialsLoader(clientKey); if (!credentials) { return { type: InstallationType.newInstallation, clientKey, }; } const { sharedSecret, storedEntity } = credentials; // Verify installation update if (rawConnectJwt) { const connectJwt = verifyConnectJwt({ rawConnectJwt, sharedSecret, unverifiedConnectJwt, }); verifyQueryStringHash({ queryStringHashType, connectJwt, computeQueryStringHashFunction: () => authDataProvider.computeQueryStringHash(baseUrl), }); return { type: InstallationType.update, clientKey, connectJwt, storedEntity, }; } throw new AuthError('Unauthorized update request', { code: AuthErrorCode.UNAUTHORIZED_REQUEST, }); } export type VerifyRequestArgs<E> = CommonVerifyArgs<E, QueryStringHashType>; export interface VerifyRequestResponse<E> { connectJwt: ConnectJwt; storedEntity: E; } /** * 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. */ export async function verifyRequest<E>({ asymmetricKeyProvider, authDataProvider, authorizationMethod = 'any', baseUrl, credentialsLoader, queryStringHashType, }: VerifyRequestArgs<E>): Promise<VerifyRequestResponse<E>> { const rawConnectJwt = authDataProvider.extractConnectJwt(); if (!rawConnectJwt) { throw new AuthError('Missing JWT', { code: AuthErrorCode.MISSING_JWT }); } // Load existing installation const unverifiedConnectJwt = decodeUnverifiedConnectJwt(rawConnectJwt); const credentials = await credentialsLoader(unverifiedConnectJwt.iss); if (!credentials) { throw new AuthError('Unknown issuer', { code: AuthErrorCode.UNKNOWN_ISSUER, unverifiedConnectJwt, }); } // Check for a signed uninstallation if ( authorizationMethod === 'publicKey' || (authorizationMethod === 'any' && isAsymmetricAlgorithm(unverifiedConnectJwt.alg)) ) { const connectJwt = await verifyAsymmetricallySignedRequest({ authDataProvider, asymmetricKeyProvider, baseUrl, queryStringHashType, unverifiedConnectJwt, }); return { connectJwt, storedEntity: credentials.storedEntity }; } const connectJwt = verifyConnectJwt({ rawConnectJwt, sharedSecret: credentials.sharedSecret, unverifiedConnectJwt, }); verifyQueryStringHash({ queryStringHashType, connectJwt, computeQueryStringHashFunction: () => authDataProvider.computeQueryStringHash(baseUrl), }); return { connectJwt, storedEntity: credentials.storedEntity }; } export interface verifySignedRequestArgs { asymmetricKeyProvider: KeyProvider; authDataProvider: AuthDataProvider; baseUrl: string; queryStringHashType?: QueryStringHashType; unverifiedConnectJwt?: ConnectJwt; } /** * Verifies a Connect request containing an asymmetrically signed JWT token. */ async function verifyAsymmetricallySignedRequest({ baseUrl, authDataProvider, asymmetricKeyProvider, queryStringHashType, unverifiedConnectJwt, }: verifySignedRequestArgs): Promise<ConnectJwt> { // Check JWT if (!unverifiedConnectJwt) { throw new AuthError('Missing JWT', { code: AuthErrorCode.MISSING_JWT }); } // Check issuer const clientKey = authDataProvider.extractClientKey(); if (unverifiedConnectJwt.iss !== clientKey) { throw new AuthError('Wrong issuer', { code: AuthErrorCode.WRONG_ISSUER, unverifiedConnectJwt, }); } // Check audience if (!unverifiedConnectJwt.aud?.includes(baseUrl)) { throw new AuthError('Wrong audience', { code: AuthErrorCode.WRONG_AUDIENCE, unverifiedConnectJwt, }); } if (!unverifiedConnectJwt.kid) { throw new AuthError('Missing token kid', { code: AuthErrorCode.MISSING_KID, unverifiedConnectJwt, }); } // Fetch public key let publicKey; try { publicKey = await asymmetricKeyProvider.get(unverifiedConnectJwt.kid, unverifiedConnectJwt); } catch (error) { throw new AuthError('Failed to obtain public key', { code: AuthErrorCode.FAILED_TO_OBTAIN_PUBLIC_KEY, originError: error, unverifiedConnectJwt, }); } // Verify asymmetric JWT const connectJwt = verifyAsymmetricConnectJwt({ rawConnectJwt: authDataProvider.extractConnectJwt(), publicKey, unverifiedConnectJwt, }); // Verify QSH verifyQueryStringHash({ queryStringHashType, connectJwt, computeQueryStringHashFunction: () => authDataProvider.computeQueryStringHash(baseUrl), }); return connectJwt; }