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:
Usage
import {
AuthError,
AuthErrorCode,
CredentialsWithEntity,
ExpressReqAuthDataProvider,
InstallationType,
verifyInstallation,
verifyRequest,
} from 'atlassian-connect-auth'
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
with credentialsLoader
.
- 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 named storedEntity
. - For new installations (when the loader callback does not return a stored entity),
verifyInstallation()
will not
return the property storedEntity
.
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 had skipQsg: true
.'computed'
: force verification using regular QSH algorithm.'context'
: force verification using static value context-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
customExtractToken
with your implementation of AuthDataProvider
.
- Implement interface
AuthDataProvider
with your own token extraction. - Extend
ExpressReqAuthDataProvider
and add new ways of extracting the token from the req
object.
For instance:
export class MyAuthDataProvider extends ExpressReqAuthDataProvider {
extractConnectJwt(): string {
const jwt = this.req.query.customJwt as string
if (jwt) {
return jwt
}
return super.extractConnectJwt()
}
}
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 the sharedSecret
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 now AuthErrorCode.MISSING_JWT
'MISSED_QSH'
is now AuthErrorCode.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
to 2.x
, if you have a direct dependency.
- This library depends on
atlassian-jwt@2.x
. - Replace
encode()
with encodeSymmetric()
or encodeAsymmetric()
. - Replace
decode()
with decodeSymmetric()
or decodeAsymmetric()
. Passing the algorithm is required.