UNPKG

@seibert/atlassian-connect-tooling

Version:

Provides authentication & utility methods for Atlassian Connect apps running on Express.

154 lines (149 loc) 7.26 kB
/// <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 {};