atlassian-connect-auth
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
Usage
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)
3.x
with signed installs
Upgrading to Addon
class was replaced with stateless function calls
Remove class instantiation and replace method calls with function calls as follows:
-
addon.auth()
⟶verifyRequest()
-
addon.install()
⟶verifyInstallation()
Also:
- Move the
baseUrl
argument from the class instantiation to the function calls. - Remove the
product
argument altogether.
Callback changes
- Replace
loadCredentials
withcredentialsLoader
.- The return value used to be any object with a required
sharedSecret
property. - Now you should return an object with a
sharedSecret
property and optionally your stored database value instoredEntity
as follows:return { sharedSecret: '...', storedEntity: databaseInstallationData, }
- The return value used to be any object with a required
- Remove
saveCredentials
from the installation verification. Use the request body payload to persist the installation data. It's safe after verifying the installation request. -
storedEntity
will 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),
Query String Hash
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.
Extracting the token from a request
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
customExtractToken
with your implementation ofAuthDataProvider
.- Implement interface
AuthDataProvider
with your own token extraction. - Extend
ExpressReqAuthDataProvider
and add new ways of extracting the token from thereq
object. 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
Signed installs and legacy verification
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 thesharedSecret
to 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
KeyProvider
interface with your HTTP client implementation. - The enum
ConnectInstallKeysCdnUrl
provides the base URLs for the Atlassian Connect CDN. - Look into
./test/keyProviderExamples
for examples of implementations using Axios, Got, and Node Fetch.
Error codes
- When checking error codes, replace string literals with values from the
AuthErrorCode
enum. - Code changes:
-
'MISSED_TOKEN'
is nowAuthErrorCode.MISSING_JWT
-
'MISSED_QSH'
is nowAuthErrorCode.MISSING_QSH
-
Upgrades in your app
- Enabled signed installs in your app descriptor. For instance:
apiMigrations: { gdpr: true, 'context-qsh': true, 'signed-install': true, },
- Upgrade
atlassian-jwt
to2.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