atlassian-connect-auth
v4.1.0
Published
Helper for handling webhooks from Atlassian products
Downloads
62
Readme
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)
Upgrading to 3.x
with signed installs
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
[email protected]
. - Replace
encode()
withencodeSymmetric()
orencodeAsymmetric()
. - Replace
decode()
withdecodeSymmetric()
ordecodeAsymmetric()
. Passing the algorithm is required.
- This library depends on