Obtain an Atlassian Cloud 3LO refresh token with Bash

Let's build a bash script that is able to obtain a refresh OAuth token for a 3LO Atlassian app to be able to make offline API requests. You can find your apps here.

The basic idea

The we create a 3LO application we get a client_id and a client_secret. We need to obtain a refresh token, so we can interact with Atlassian. To do so, we need to ask the user to click on a URL, confirm our access and wait for Atlassian to give us the token. We need to configure the app to send the auth code to http://localhost:8014/token.

sequenceDiagram

autonumber
participant P as Alice
participant S as Script
participant A as Atlassian
participant API as API

P->>S:./refresh.sh

S->>S:start netcat on port 8014
S->>P:display URL for<br/>token request
P->>A:open URL in browser
P->>A:confirm token
A->>P:redirect http://localhost:8014/token
S->>S:detects token on port 8014
S->>API:authorization_code
S->>S:save result to token file

As you can see, we'll listen with netcat on port 8014 and save the result. Now that we've saved the details to a token file, we can use the file to refresh the token for any follow up requests.

sequenceDiagram

autonumber
participant P as Alice
participant S as Script
participant A as Atlassian
participant API as API

P->>S:./refresh.sh
S->>S:read refresh token from file
S->>API:refresh_token
API->>S:token
S->>S:save result to token file

We'll store some files next to the script:

  • .env a file with the environment variables containing the app secrets and script settings.
  • token.json a file that contains the refresh token details.

Prerequisites

We need to make sure we install some tools the script uses. We'll use jq for handling JSON, uuid-runtime for GUID generation and netcat to capture the OAuth code from Jira.

apt-get update
apt-get install -y uuid-runtime jq netcat

Environment file

First, we need to store the settings of our scripts somewhere. As it is bad practice to add keys to the script itself, we'll create a .env file. You might consider using environment settings in a production-like environment.

JIRA_CLIENT_ID=""
JIRA_CLIENT_SECRET=""
JIRA_DOMAIN="https://my-url.atlassian.net"
JIRA_SCOPE="offline_access offline_access read:jira-work read:jira-user write:jira-work"
REFRESH_PORT=8014
TOKEN_FILE_NAME="token.json"

Note the JIRA_SCOPE, this defines what we want to do with the token. In order to get a refresh token, we'll need to request offline_access.

The script

Here's the final script:

#!/bin/bash
set -e

# settings
ENV_FILE="jira.env"

# colors
BLUE='\033[1;34m'
NC='\033[0m'

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
pushd "$SCRIPT_DIR" &>/dev/null

# validate dependencies
for cmd in jq nc uuidgen; do
  if ! type "$cmd" &>/dev/null; then
    echo "Error: '$cmd' is not installed. Please install it to continue."
    exit 1
  fi
done

if [ ! -f "$ENV_FILE" ]; then
  echo "Error: '$ENV_FILE' is missing, please create it."
  exit 1
elif grep -q $'\r' "$ENV_FILE"; then
  echo "Error: $ENV_FILE file contains Windows-style line endings (\r)."
  exit 1
else
  set -a
  source "$ENV_FILE"
  set +a
fi

# Function to print header
print_header() {
  echo -e "
R3FR3SH ${BLUE}T0K3N${NC} G3N3R@T0R 4
     __ ${BLUE}__ ${NC}
    |__${BLUE}|__|${NC}___________
    |  |  \\_  __ \\__  \\
    |  |  ||  | \\// __ \\_
/\\__|  |__||__|  (____  ${BLUE}/${NC}
\\______|              ${BLUE}\\/${NC}
"
}

# Function for POST requests
post_request() {
  local url=$1
  local data=$2
  curl --request POST "$url" \
    --header "Content-Type: application/json" \
    --data "$data" \
    --silent
}

# Function to refresh token
refresh_token() {
  local refresh_token data response

  refresh_token=$(jq '.refresh_token' --raw-output <"$TOKEN_FILE_PATH")
  data='{
    "grant_type": "refresh_token",
    "client_id": "'$JIRA_CLIENT_ID'",
    "client_secret": "'$JIRA_CLIENT_SECRET'",
    "refresh_token": "'$refresh_token'"
  }'
  printf "Refreshing token..."
  response=$(post_request "https://auth.atlassian.com/oauth/token" "$data")
  echo "$response" | jq >"$TOKEN_FILE_PATH"
  echo -e " ${BLUE}OK${NC}"
  echo -e "Your refresh token is saved to ${BLUE}${TOKEN_FILE_NAME}${NC}."
  echo
}

# Function to generate new token
generate_token() {

  local uuid redirect_url redirect_url_2 jira_scope jira_url request response code msg

  uuid="$(uuidgen)"
  redirect_url="http%3A%2F%2Flocalhost%3A$REFRESH_PORT"
  redirect_url_2="http://localhost:$REFRESH_PORT"
  jira_scope=$(echo "$JIRA_SCOPE" | sed 's/ /%20/g' | sed 's/:/%3A/g')
  jira_url="https://auth.atlassian.com/authorize?audience=api.atlassian.com&client_id=$JIRA_CLIENT_ID&scope=$jira_scope&state=$uuid&response_type=code&prompt=consent&redirect_uri=$redirect_url"

  echo -e -n "Click link to authorize app:$BLUE
$jira_url
$NC
Waiting for confirmation..."

  code=""
  msg="OK, you can close this website."
  while true; do
    request=$(echo -e "HTTP/1.1 200 OK\r\nContent-Length: $(echo -n "$msg" | wc -c)\r\n\r\n$msg" | nc -N -l -p "$REFRESH_PORT")
    code=$(echo "$request" | grep "$uuid" | grep -oP 'code=\K[^& ]*')
    if [[ -n "$code" ]]; then
      break
    fi
  done

  echo -e " ${BLUE}OK${NC}"
  echo -n "Getting refresh token..."
  local data='{
    "grant_type": "authorization_code",
    "client_id": "'$JIRA_CLIENT_ID'",
    "client_secret": "'$JIRA_CLIENT_SECRET'",
    "redirect_uri": "'$redirect_url_2'",
    "code": "'$code'"
  }'
  response=$(post_request "https://auth.atlassian.com/oauth/token" "$data")
  echo "$response" | jq >"$TOKEN_FILE_PATH"
  echo -e " ${BLUE}OK${NC}"
  echo -e "Your refresh token is saved to ${BLUE}${TOKEN_FILE_NAME}${NC}."
  echo
}

print_header

TOKEN_FILE_PATH="$SCRIPT_DIR/$TOKEN_FILE_NAME"
if [ -f "$TOKEN_FILE_PATH" ]; then
  refresh_token
else
  generate_token
fi

Changelog

  • Added some extra checks to the scripts to check if the dependencies are installed and if the .env has \r characters (thanks Windows!). We've swapped printf out for echo.
  • Initial article.
expand_less brightness_auto