@seibert/atlassian-connect-tooling
Version:
Provides authentication & utility methods for Atlassian Connect apps running on Express.
154 lines (149 loc) • 7.26 kB
TypeScript
/// <reference types="node" />
/// <reference types="qs" />
/// <reference types="range-parser" />
import { Request as AtlassianJwtRequest } from 'atlassian-jwt';
import { NextFunction, Request as ExpressRequest, Response as ExpressResponse } from 'express-serve-static-core';
import { JwtPayload } from 'jsonwebtoken';
export interface AtlassianConnectConfig {
baseUrl: string;
}
export type TenantDataWithSharedSecret = {
sharedSecret: string;
} & unknown;
export type TenantDataWithClientKey = {
clientKey: string;
} & unknown;
export type TenantByClientKeySupplier = (clientKey: string) => Promise<TenantDataWithSharedSecret | null>;
export interface AtlassianConnectAuthenticationMiddlewareConfig {
fetchTenantByClientKey: TenantByClientKeySupplier;
requestsForQshValidation?: AtlassianJwtRequest[];
}
export type AuthenticatedInstallationRequest = ExpressRequest & {
validatedJwtPayload: JwtPayload;
};
/**
* Returns a middleware that can be used for authentication of install and uninstall lifecycle hook requests in Atlassian Connect ecosystem.
* The composed middleware verifies the requests as outlined in the Atlassian Connect documentation using a asymmetric encryption and a RSA public key
* provided from an Atlassian Host.
*
* @see https://community.developer.atlassian.com/t/action-required-atlassian-connect-installation-lifecycle-security-improvements/49046
* @see https://developer.atlassian.com/cloud/confluence/understanding-jwt/
*
* You have to provide the baseUrl of your app. This is most probably the host you provide as a "baseUrl" in your atlassian-connect.json.
*
* Example usage:
*
* app.post('/lifecycle/installed/', [composeAtlassianConnectInstallationMiddleware({baseUrl: ATLASSIAN_CONNECT_FILE.baseUrl})], async (req: Request & ReqWithFirebaseUser, res: Response) => {
* console.log('Atlassian Connect Lifecycle Hook triggered: /installed', req.body);
* await persistInstallation(db, req.body);
* res.send();
* });
*
* @param baseUrl
* baseUrl from your atlassian-connect.json.
*/
export declare function composeAtlassianConnectInstallationMiddleware({ baseUrl }: AtlassianConnectConfig): (req: AuthenticatedInstallationRequest, res: ExpressResponse, next: NextFunction) => void;
export type AuthenticatedAtlassianRequest = ExpressRequest & {
atlassianVerified: {
userAccountId: string;
clientKey: string;
tenant: TenantDataWithSharedSecret;
jwtPayload: JwtPayload;
};
};
/**
* Returns a middleware that can be used for authentication of user requests.
* The composed middleware verifies the requests as outlined in the Atlassian Connect documentation using a JWT signed with the sharedSecret of a tenant.
*
* @see https://developer.atlassian.com/cloud/confluence/understanding-jwt/
* @param baseUrl
* BaseUrl from your atlassian-connect.json.
* @param fetchTenantByClientKey
* Method that asynchronously returns the data that you have stored for the tenant given its clientKey. Returned data has to contain at least a "sharedSecret".
* Return null if no matching tenant can be found by your implementation.
* @param requestsForQshValidation (optional)
* The default behaviour of the returned middleware is to use the actual Express request that is passed into it to validate the given JWTs query hash (qsh claim).
* You can optionally choose to validate the given qsh against another fictitious requests. If one request matches the qsh, the request is assumed to be authenticated.
* This functionality can be used in combination with createJwtForRequestAuthorization which allows you to create JWTs for such fictitious requests.
* A client can receive such JWTs and use it to make requests to endpoints protected by the returned middleware.
*
* @see create-jwt.ts for code examples
*/
export declare function composeAtlassianRequestAuthenticationMiddleware({ baseUrl, fetchTenantByClientKey, requestsForQshValidation }: AtlassianConnectConfig & AtlassianConnectAuthenticationMiddlewareConfig): (req: AuthenticatedAtlassianRequest, res: ExpressResponse, next: NextFunction) => void;
/**
* Creates a JWT that can be used for authentication of requests by providing it in the Authorization header as "JWT xxx".
* @param tenantData
* Data object you have stored for the Atlassian tenant. It has to contain a sharedSecret (used for JWT signing) and a
* clientKey (used as the JWT audience).
* @param issuer
* Issuer string to be passed as the JWTs iss claim. Should be set to the app-key if you use this token for api calls
* in server-to-server communication with atlassian host products.
* @param expirationInSeconds
* JWT expiration time in seconds. defaults to 300.
* @param method
* HTTP method of the signed request (GET, POST, ...). defaults to GET.
* @param pathname
* Pathname of the signed request (sth. like "/rest/api/content"), used to generate query hash (qsh claim).
* @param body
* Body of the signed request, used to generate query hash (qsh claim).
* @param query
* Query params of the signed request, used to generate query hash (qsh claim).
* @param baseUrl
* BaseUrl of the requests. Gets passed as baseUrl param to Atlassians jwt library. Can be omitted if pathname is set correctly
* @param additionalClaims
* Additional claims to be appended to JWT payload. Can be used to override aspects of the JWT or to enrich it with additional context information like a userId
*
* @example
* usage for api calls to confluence
* ```ts
* const jwt = createJwtForRequestAuthorization({
* issuer: "my-app-key",
* request: {pathname: "rest/api/content", method: "GET"},
* tenantData: {
* clientKey: "tenant-key-of-installation-cb1f7a7ee297",
* sharedSecret: "sharedSecretOfClientFromInstallationProcess"
* }
* });
* ```
*
* @example
* usage for authentication against middleware function from authenticate-middleware.ts with fictitious path
* ```ts
* // 1. generate jwt
* const jwt = createJwtForRequestAuthorization({
* issuer: "tenant-key-of-installation-cb1f7a7ee297",
* request: {pathname: "/admin-only", method: "GET"},
* tenantData: {
* clientKey: "tenant-key",
* sharedSecret: "tenants-shared-secret"
* }
* });
*
* // ... pass it to the client ...
*
* // 2. validate with middleware
* const middleware = composeAtlassianRequestAuthenticationMiddleware({
* baseUrl: "https://your.apps.base.url",
* fetchTenantByClientKey: (clientKey: string) => Promise.resolve({
* clientKey: "tenant-key",
* sharedSecret: "tenants-shared-secret"
* }),
* requestsForQshValidation: [{method: "GET", pathname: "/admin-only"}]
* });
*
* // 3. use middleware for authentication
* app.post("/some/endpoint/", [middleware], async(req: Request, res: Response) => {
* res.send();
* });
* ```
**/
export declare function createJwtForRequestAuthorization({ tenantData, issuer, expirationInSeconds, request: { method, pathname, body, query }, baseUrl, additionalClaims, }: CreateJwtForRequestParams): string;
export interface CreateJwtForRequestParams {
tenantData: TenantDataWithSharedSecret & TenantDataWithClientKey;
issuer: string;
expirationInSeconds?: number;
request: Partial<AtlassianJwtRequest>;
baseUrl?: string;
additionalClaims?: Partial<JwtPayload>;
}
export {};