This library implements authentication for installation requests, webhooks, and page loading from Atlassian products built with Connect.
For a deeper understanding of the concepts built into this library, please read through the Atlassian Connect documentation for the corresponding product:
- Jira Cloud: Understanding JWT for Connect apps
- Confluence Cloud: Understanding JWT for Connect apps
- Bitbucket App: Understanding JWT for apps
import {
AuthError,
AuthErrorCode,
CredentialsWithEntity,
ExpressReqAuthDataProvider,
InstallationType,
verifyInstallation,
verifyRequest,
} from 'atlassian-connect-auth'
// Consumers of this library have to provide a KeyProvider implementation that will fetch the public key from a CDN.
// Examples can be found under the `test` directory in this library.
import { GotKeyProvider } from './GotKeyProvider';
const baseUrl = 'https://your-app-base-url.com'
const asymmetricKeyProvider = new GotKeyProvider()
async function loadInstallationEntity(clientKey: string): Promise<CredentialsWithEntity<InstallationEntity>> {
const storedEntity = await model.InstallationEntity.findOne({ where: { clientKey } })
if (storedEntity) {
return {
sharedSecret: decrypt(storedEntity.encryptedSharedSecret),
storedEntity,
}
}
}
const handleInstallation = async (req, res) => {
try {
const result = await verifyInstallation({
baseUrl,
asymmetricKeyProvider,
authDataProvider: new ExpressReqAuthDataProvider(req),
credentialsLoader: loadInstallationEntity,
})
const newInstallationEntity = req.body
if (result.type === InstallationType.update) {
const existingInstallationEntity = result.storedEntity
await existingInstallationEntity.update(newInstallationEntity)
} else {
await model.InstallationEntity.create(newInstallationEntity)
}
res.sendStatus(201)
} catch (error) {
if (error instanceof AuthError) {
console.warn(error)
res.sendStatus(401)
} else {
console.error(error)
res.sendStatus(500)
}
}
}
const handleAuth = async (req, res, next) => {
try {
const { connectJwt, storedEntity } = await verifyRequest({
baseUrl,
asymmetricKeyProvider,
authDataProvider: new ExpressReqAuthDataProvider(req),
credentialsLoader: loadInstallationEntity,
queryStringHashType: 'context',
})
req.context = {
accountId: connectJwt.context?.user?.accountId ?? connectJwt.sub,
installationData: storedEntity
}
next()
} catch (error) {
if (error instanceof AuthError) {
console.warn(error)
res.sendStatus(401)
} else {
console.error(error)
res.sendStatus(500)
}
}
}
const app = express()
.post('/api/hooks/jira/installed', handleInstall)
.post('/api/hooks/jira/uninstalled', handleAuth, handleUninstall)
.post('/api/hooks/jira/project/created', handleAuth, handleProjectCreated)Remove class instantiation and replace method calls with function calls as follows:
addon.auth()⟶verifyRequest()addon.install()⟶verifyInstallation()
Also:
- Move the
baseUrlargument from the class instantiation to the function calls. - Remove the
productargument altogether.
- Replace
loadCredentialswithcredentialsLoader.- The return value used to be any object with a required
sharedSecretproperty. - Now you should return an object with a
sharedSecretproperty and optionally your stored database value instoredEntityas follows:return { sharedSecret: '...', storedEntity: databaseInstallationData, }
- The return value used to be any object with a required
- Remove
saveCredentialsfrom the installation verification. Use the request body payload to persist the installation data. It's safe after verifying the installation request. storedEntitywill be returned by the verification function if a value is provided.- For installation updates (when the loader callback returns a stored entity),
verifyInstallation()will return the loaded entity with an attribute also namedstoredEntity. - For new installations (when the loader callback does not return a stored entity),
verifyInstallation()will not return the propertystoredEntity.
- For installation updates (when the loader callback returns a stored entity),
Replace the argument skipQsh with queryStringHashType, which is an enum with
the following values:
'skip': skip QSH verification altogether. Use this in routes you hadskipQsg: true.'computed': force verification using regular QSH algorithm.'context': force verification using static valuecontext-qsh.'any': accepts both'computed'and'context'.
Note: Bitbucket Cloud does not currently support context-qsh as it does not have a JavaScript API that
allows generating a context token.
Version 2.x took a request object as the first argument of the verifications functions. It expected
an Express.js-like request object in order to extract the token from headers or query arguments.
Version 3.x decouples that from the web framework with the authDataProvider parameter.
- Remove the first argument with the request object.
- Provide an implementation of
authDataProvider.- For Express.js, use provided
ExpressReqAuthDataProvider. Example:verifyRequest({ authDataProvider: new ExpressReqAuthDataProvider(req), ... })
- Replace custom token extraction with
customExtractTokenwith your implementation ofAuthDataProvider.- Implement interface
AuthDataProviderwith your own token extraction. - Extend
ExpressReqAuthDataProviderand add new ways of extracting the token from thereqobject. For instance:
export class MyAuthDataProvider extends ExpressReqAuthDataProvider { extractConnectJwt(): string { // Custom query argument const jwt = this.req.query.customJwt as string if (jwt) { return jwt } // fallback to regular Connect token extraction return super.extractConnectJwt() } }
- Implement interface
- For Express.js, use provided
Add the authorizationMethod argument to the verification methods to define how
you want installations to be verified.
sharedSecret: force legacy method that won't check new installations and will use thesharedSecretto verify installation updates and uninstallations.publicKey: force new signed installs that use a public key to verify new installations, installation updates, and uninstallations.any: accept both verification methods, meant to be used during the transition period. This is the default value.
Note: Bitbucket Cloud does not support signed installs as of 2021. You can still upgrade the library and keep it in compatibility mode (accepting legacy installs) as a preparation for a future upgrade.
Signed installations need to download a public key from the Atlassian Connect CDN. You need to provide an
asymmetricKeyProvider to the verification functions.
- Implement the
KeyProviderinterface with your HTTP client implementation. - The enum
ConnectInstallKeysCdnUrlprovides the base URLs for the Atlassian Connect CDN. - Look into
./test/keyProviderExamplesfor examples of implementations using Axios, Got, and Node Fetch.
- When checking error codes, replace string literals with values from the
AuthErrorCodeenum. - Code changes:
'MISSED_TOKEN'is nowAuthErrorCode.MISSING_JWT'MISSED_QSH'is nowAuthErrorCode.MISSING_QSH
- Enabled signed installs in your app descriptor. For instance:
apiMigrations: { gdpr: true, 'context-qsh': true, 'signed-install': true, },
- Upgrade
atlassian-jwtto2.x, if you have a direct dependency.- This library depends on
atlassian-jwt@2.x. - Replace
encode()withencodeSymmetric()orencodeAsymmetric(). - Replace
decode()withdecodeSymmetric()ordecodeAsymmetric(). Passing the algorithm is required.
- This library depends on