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.

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://172.0.0.1: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://127.0.0.1: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 read:build:jira-software write:build:jira-software read:build-info:jira write:build-info:jira read:issue:jira read:issue:jira-software read:epic:jira-software read:issue-details:jira read:field.default-value:jira read:field.option:jira read:field:jira read:group:jira read:jira-work"
REFRESH_PORT=8014
REFRESH_PATH="/token"
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

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

# Load env file
set -a
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
source "$SCRIPT_DIR/.env"
set +a

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

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

# Function to generate new token
generate_token() {
  local uuid=$(uuidgen)
  local scope=$(echo "$JIRA_SCOPE" | sed 's/ /%20/g' | sed 's/:/%3A/g')
  local jira_url="https://auth.atlassian.com/authorize?audience=api.atlassian.com&client_id=$JIRA_CLIENT_ID&scope=$scope&state=$uuid&response_type=code&prompt=consent"
  printf "Click link to authorize app:${BLUE}\n"
  printf "%s\n\n" "$jira_url"
  printf "${NC}Waiting for confirmation..."
  local code=""
  local msg="OK, you can close this website."
  while true; do
      local 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
  printf " ${BLUE}OK${NC}\n"
  printf "Getting refresh token..."
  local data='{
    "grant_type": "authorization_code",
    "client_id": "'$JIRA_CLIENT_ID'",
    "client_secret": "'$JIRA_CLIENT_SECRET'",
    "code": "'$code'"
  }'
  local response=$(post_request "https://auth.atlassian.com/oauth/token" "$data")
  echo "$response" | jq > "$TOKEN_FILE_PATH"
  printf " ${BLUE}OK${NC}\n"
  printf "Your refresh token is saved to ${BLUE}$TOKEN_FILE_NAME${NC}.\n\n"
}

print_header

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