Skip to content

Instantly share code, notes, and snippets.

@joshbalfour
Last active February 25, 2022 16:00
Show Gist options
  • Save joshbalfour/0435fd7f7a3675504d269c18154f78fe to your computer and use it in GitHub Desktop.
Save joshbalfour/0435fd7f7a3675504d269c18154f78fe to your computer and use it in GitHub Desktop.
How to use the Bitbucket API to generate an access token as an app or "add-on" using JWT auth, and download a repo

This is an example of how to use the Bitbucket API with JWT auth to generate an access token, then use it to download a repo. I couldn't find a working example online anywhere so I made one to hopefully save you some time.

This assumes that you've made a bitbucket app and have ngrok installed locally.

  1. Grab your client secret from the app settings
  2. Update atlassian-connect.json with a key you make up (must be globally unique i think)
  3. Run ngrok http 3000
  4. Run this script with the client secret and the ngrok url: CLIENT_SECRET=secret PUBLIC_URL=httpsNgrokUrl yarn start (you can replace yarn here with npm if you like)
  5. Go to the installation url for your app and install the app, you should see a json dump on the terminal
  6. Find 'clientKey' in the json and pass that to the "go" function, along with info of the repo you want to download (owner, name, branch)
{
"key": "random-key",
"name": "Example App",
"description": "An example app for Bitbucket",
"vendor": {
"name": "Angry Nerds",
"url": "https://www.atlassian.com/angrynerds"
},
"baseUrl": "populated-at-runtime",
"authentication": {
"type": "jwt"
},
"lifecycle": {
"installed": "/installed",
"uninstalled": "/uninstalled"
},
"modules": {
"webhooks": [
{
"event": "*",
"url": "/webhook"
}
],
"webItems": [
{
"url": "http://example.com?repoPath={repository.full_name}",
"name": {
"value": "Example Web Item"
},
"location": "org.bitbucket.repository.navigation",
"key": "example-web-item",
"params": {
"auiIcon": "aui-iconfont-link"
}
}
],
"repoPages": [
{
"url": "/connect-example?repoPath={repository.full_name}",
"name": {
"value": "Example Page"
},
"location": "org.bitbucket.repository.navigation",
"key": "example-repo-page",
"params": {
"auiIcon": "aui-iconfont-doc"
}
}
],
"webPanels": [
{
"url": "/connect-example?repoPath={repository.full_name}",
"name": {
"value": "Example Web Panel"
},
"location": "org.bitbucket.repository.overview.informationPanel",
"key": "example-web-panel"
}
]
},
"scopes": ["account", "repository"],
"contexts": ["account"]
}
import express from 'express'
import bodyParser from 'body-parser'
import * as jwt from 'atlassian-jwt'
import fetch from 'node-fetch'
import fs from 'fs/promises'
import descriptor from './atlassian-connect.json'
const {
CLIENT_SECRET = '',
PUBLIC_URL = '',
} = process.env
const app = express()
app.use(bodyParser.json())
app.get('/', (req, res) => {
console.log('/')
res.redirect('/atlassian-connect.json')
})
app.post('/uninstalled', (req, res) => {
console.log('uninstalled')
console.log(req.body)
res.sendStatus(200)
})
app.post('/installed', (req, res) => {
console.log('installed')
console.log(req.body) // store at least { clientKey }
res.sendStatus(200)
})
app.get('/atlassian-connect.json', (req, res) => {
console.log('descriptor')
descriptor.baseUrl = PUBLIC_URL
res.json(descriptor)
})
app.listen(3000, () => {
console.log('listening on port 3000')
})
const qs = (params: Record<string, any>) => {
const sp = new URLSearchParams(params)
return sp.toString()
}
const getAccessToken = async (key: string, clientKey: string, secret: string) => {
const baseUrl = 'https://bitbucket.org'
const url = 'https://bitbucket.org/site/oauth2/access_token'
const opts = {
method: 'post' as 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: {
grant_type: 'urn:bitbucket:oauth2:jwt',
},
}
const req = jwt.fromMethodAndPathAndBody(opts.method, url, opts.body)
const qsh = jwt.createQueryStringHash(req, true, baseUrl)
const now = Math.floor(Date.now() / 1000)
const exp = now + (3 * 60) // expires in 3 minutes
const tokenData = {
iss: key,
iat: now, // the time the token is generated
exp, // token expiry time
sub: clientKey, // clientKey from /installed
qsh,
}
const jwtToken = jwt.encodeSymmetric(tokenData, secret)
const options = {
method: opts.method,
headers: {
...opts.headers,
Authorization: `JWT ${jwtToken}`
},
body: qs(opts.body),
}
const res = await fetch(url, options)
if (res.status !== 200) {
throw new Error(`Failed to get access token: ${res.status}`)
}
return res.json()
}
const downloadRepo = async (owner: string, name: string, branch: string, accessToken: string) => {
const url = `https://bitbucket.org/${owner}/${name}/get/${branch}.zip`
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (res.status !== 200) {
throw new Error(`Failed to download repo: ${res.status}`)
}
const zip = await res.buffer()
return zip
}
const go = async (clientKey: string, repoOwner: string, repoName: string, branchName: string) => {
const token = await getAccessToken(descriptor.key, clientKey, CLIENT_SECRET)
const zipBuffer = await downloadRepo(repoOwner, repoName, branchName, token.access_token)
await fs.writeFile(`${repoOwner}-${repoName}-${branchName}.zip`, zipBuffer)
}
// go().then(console.log).catch(console.error)
{
"name": "bitbucket-jwt-example",
"version": "1.0.0",
"main": "index.ts",
"license": "MIT",
"scripts": {
"start": "ts-node-dev index.ts"
},
"dependencies": {
"atlassian-jwt": "^2.0.2",
"body-parser": "^1.19.2",
"express": "^4.17.3",
"node-fetch": "^2.6.7",
"querystring": "^0.2.1"
},
"devDependencies": {
"@tsconfig/node12": "^1.0.9",
"@types/express": "^4.17.13",
"@types/node-fetch": "^2.6.1",
"ts-node-dev": "^1.1.8",
"typescript": "^4.5.5"
}
}
{
"extends": "@tsconfig/node12/tsconfig.json",
"compilerOptions": {
"preserveConstEnums": true,
"resolveJsonModule": true,
},
"include": ["**/*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment