Skip to content

Auto-refresh Token cho IntelliJ GraphQL Plugin

Khi execute GraphQL queries trực tiếp trong IntelliJ, token thường có TTL ngắn và cần được refresh định kỳ. Bài này ghi lại cách setup script tự động refresh token và tích hợp với GraphQL plugin.

Vấn đề

  • IntelliJ GraphQL plugin không có pre-request hook để tự refresh token
  • Access token TTL ngắn (ví dụ 5 phút), refresh token TTL trung bình (30 phút)
  • Mỗi lần token hết hạn phải thủ công update config → tốn thời gian

Giải pháp

Dùng .env.local + script bash để tự động refresh token chạy ngầm trong terminal.

Cấu trúc file

project/
├── .graphqlrc.yml         ← hoặc graphql.config.yml
├── .env.local             ← credentials + tokens (gitignore)
└── scripts/
    ├── gql-login.sh       ← chạy thủ công 1 lần khi bắt đầu
    ├── gql-refresh-token.sh
    └── gql-watch-token.sh ← chạy ngầm suốt session làm việc

Config

.graphqlrc.yml

yaml
schema: schema.graphql
extensions:
  endpoints:
    default:
      url: ${GQL_ENDPOINT}
      headers:
        Authorization: "Bearer ${GQL_TOKEN}"

Syntax env variable

Plugin dùng ${VARIABLE_NAME}không có prefix env:.
${env:GQL_TOKEN} sẽ bị parse thành variable tên env, không đọc được từ .env file.

.env.local

bash
GQL_ENDPOINT=https://your-api/graphql
GQL_BRAND_ID=brand_xxx
GQL_USERNAME=your-email@example.com
GQL_PASSWORD=your-password
GQL_TOKEN=                          # auto-filled by script
GQL_TOKEN_EXPIRES_AT=               # auto-filled by script
GQL_REFRESH_TOKEN=                  # auto-filled by script
GQL_REFRESH_EXPIRES_AT=             # auto-filled by script

.gitignore

.env
.env.local

Tại sao dùng .env.local thay vì .env?

Plugin đọc file theo thứ tự ưu tiên từ cao xuống thấp:
.env.local.env.development.local.env.development.env.dev.env

Plugin dừng lại ở file có priority cao nhất tìm thấy — nếu .env.local tồn tại,
.env sẽ không được đọc. Vì vậy nên để tất cả variables trong cùng 1 file.

Scripts

gql-login.sh

Đọc credentials từ .env.local, gọi mutation login, lưu tokens vào .env.local.

bash
#!/bin/bash
set -e

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"

if [ -f "$ROOT_DIR/.env.local" ]; then
  export $(grep -v '^#' "$ROOT_DIR/.env.local" | xargs)
fi

if [ -z "$GQL_ENDPOINT" ] || [ -z "$GQL_BRAND_ID" ] || \
   [ -z "$GQL_USERNAME" ] || [ -z "$GQL_PASSWORD" ]; then
  echo "❌ Thiếu config trong .env.local (GQL_ENDPOINT, GQL_BRAND_ID, GQL_USERNAME, GQL_PASSWORD)"
  exit 1
fi

echo "Logging in as $GQL_USERNAME (brand: $GQL_BRAND_ID)..."

RESPONSE=$(curl -s -X POST "$GQL_ENDPOINT" \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
    --arg brandId "$GQL_BRAND_ID" \
    --arg username "$GQL_USERNAME" \
    --arg password "$GQL_PASSWORD" \
    '{
      query: "mutation Login($input: LoginInput!) { login(input: $input) { accessToken expiresAt refreshToken sessionId userId } }",
      variables: { input: { brandId: $brandId, username: $username, password: $password } }
    }'
  )")

GQL_ERROR=$(echo "$RESPONSE" | jq -r '.errors[0].message // empty')
if [ -n "$GQL_ERROR" ]; then
  echo "❌ Login thất bại: $GQL_ERROR"
  exit 1
fi

ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r '.data.login.accessToken')
REFRESH_TOKEN=$(echo "$RESPONSE" | jq -r '.data.login.refreshToken')
EXPIRES_AT=$(echo "$RESPONSE" | jq -r '.data.login.expiresAt')
USER_ID=$(echo "$RESPONSE" | jq -r '.data.login.userId')
SESSION_ID=$(echo "$RESPONSE" | jq -r '.data.login.sessionId')

if [ "$ACCESS_TOKEN" = "null" ] || [ -z "$ACCESS_TOKEN" ]; then
  echo "❌ Không lấy được accessToken"
  echo "$RESPONSE" | jq .
  exit 1
fi

REFRESH_EXPIRES_AT=$(date -u -d "+30 minutes" "+%Y-%m-%dT%H:%M:%SZ" 2>/dev/null \
  || date -u -v+30M "+%Y-%m-%dT%H:%M:%SZ")

sed -i.bak \
  -e "s|GQL_TOKEN=.*|GQL_TOKEN=$ACCESS_TOKEN|" \
  -e "s|GQL_TOKEN_EXPIRES_AT=.*|GQL_TOKEN_EXPIRES_AT=$EXPIRES_AT|" \
  -e "s|GQL_REFRESH_TOKEN=.*|GQL_REFRESH_TOKEN=$REFRESH_TOKEN|" \
  -e "s|GQL_REFRESH_EXPIRES_AT=.*|GQL_REFRESH_EXPIRES_AT=$REFRESH_EXPIRES_AT|" \
  "$ROOT_DIR/.env.local"
rm -f "$ROOT_DIR/.env.local.bak"

echo ""
echo "✅ Login thành công"
echo "   User ID    : $USER_ID"
echo "   Session ID : $SESSION_ID"
echo "   Access token expires : $EXPIRES_AT"
echo "   Refresh token expires: $REFRESH_EXPIRES_AT"

gql-refresh-token.sh

bash
#!/bin/bash
set -e

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"

if [ -f "$ROOT_DIR/.env.local" ]; then
  export $(grep -v '^#' "$ROOT_DIR/.env.local" | xargs)
else
  echo "❌ .env.local not found. Chạy gql-login.sh trước."
  exit 1
fi

if [ -z "$GQL_REFRESH_TOKEN" ]; then
  echo "❌ GQL_REFRESH_TOKEN is not set. Chạy gql-login.sh trước."
  exit 1
fi

# Check refresh token còn hạn không
if [ -n "$GQL_REFRESH_EXPIRES_AT" ]; then
  NOW=$(date -u +%s)
  CLEAN_EXPIRES=$(echo "$GQL_REFRESH_EXPIRES_AT" | sed 's/\..*//' | sed 's/Z//')
  EXPIRES=$(date -u -d "${CLEAN_EXPIRES}" +%s 2>/dev/null \
    || date -u -j -f "%Y-%m-%dT%H:%M:%S" "${CLEAN_EXPIRES}" +%s)

  if [ "$NOW" -ge "$EXPIRES" ]; then
    echo "❌ Refresh token đã hết hạn. Cần login lại:"
    echo "   ./scripts/gql-login.sh"
    exit 1
  fi
fi

RESPONSE=$(curl -s -X POST "$GQL_ENDPOINT" \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
    --arg refreshToken "$GQL_REFRESH_TOKEN" \
    '{
      query: "mutation RefreshToken($input: RefreshTokenInput!) { refreshToken(input: $input) { accessToken refreshToken expiresAt } }",
      variables: { input: { refreshToken: $refreshToken } }
    }'
  )")

GQL_ERROR=$(echo "$RESPONSE" | jq -r '.errors[0].message // empty')
if [ -n "$GQL_ERROR" ]; then
  echo "❌ Refresh thất bại: $GQL_ERROR. Cần login lại:"
  echo "   ./scripts/gql-login.sh"
  exit 1
fi

ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r '.data.refreshToken.accessToken')
NEW_REFRESH_TOKEN=$(echo "$RESPONSE" | jq -r '.data.refreshToken.refreshToken')
EXPIRES_AT=$(echo "$RESPONSE" | jq -r '.data.refreshToken.expiresAt')

if [ "$ACCESS_TOKEN" = "null" ] || [ -z "$ACCESS_TOKEN" ]; then
  echo "❌ Không lấy được accessToken"
  echo "$RESPONSE" | jq .
  exit 1
fi

REFRESH_EXPIRES_AT=$(date -u -d "+30 minutes" "+%Y-%m-%dT%H:%M:%SZ" 2>/dev/null \
  || date -u -v+30M "+%Y-%m-%dT%H:%M:%SZ")

sed -i.bak \
  -e "s|GQL_TOKEN=.*|GQL_TOKEN=$ACCESS_TOKEN|" \
  -e "s|GQL_TOKEN_EXPIRES_AT=.*|GQL_TOKEN_EXPIRES_AT=$EXPIRES_AT|" \
  -e "s|GQL_REFRESH_TOKEN=.*|GQL_REFRESH_TOKEN=$NEW_REFRESH_TOKEN|" \
  -e "s|GQL_REFRESH_EXPIRES_AT=.*|GQL_REFRESH_EXPIRES_AT=$REFRESH_EXPIRES_AT|" \
  "$ROOT_DIR/.env.local"
rm -f "$ROOT_DIR/.env.local.bak"

echo "✅ Token refreshed. Access token expires: $EXPIRES_AT"

gql-watch-token.sh

Chạy ngầm trong terminal, tự động gọi gql-refresh-token.sh khi access token còn dưới 1 phút.

bash
#!/bin/bash

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"

echo "👀 Watching token... (Ctrl+C để dừng)"

while true; do
  export $(grep -v '^#' "$ROOT_DIR/.env.local" | xargs)

  NOW=$(date -u +%s)

  # Check refresh token — nếu hết thì exit
  CLEAN_REFRESH_EXPIRES=$(echo "$GQL_REFRESH_EXPIRES_AT" | sed 's/\..*//' | sed 's/Z//')
  REFRESH_EXPIRES=$(date -u -d "${CLEAN_REFRESH_EXPIRES}" +%s 2>/dev/null \
    || date -u -j -f "%Y-%m-%dT%H:%M:%S" "${CLEAN_REFRESH_EXPIRES}" +%s)

  if [ "$NOW" -ge "$REFRESH_EXPIRES" ]; then
    echo "❌ $(date '+%H:%M:%S') Refresh token hết hạn. Cần login lại: ./scripts/gql-login.sh"
    exit 1
  fi

  # Check access token — nếu còn < 1p thì refresh
  CLEAN_TOKEN_EXPIRES=$(echo "$GQL_TOKEN_EXPIRES_AT" | sed 's/\..*//' | sed 's/Z//')
  TOKEN_EXPIRES=$(date -u -d "${CLEAN_TOKEN_EXPIRES}" +%s 2>/dev/null \
    || date -u -j -f "%Y-%m-%dT%H:%M:%S" "${CLEAN_TOKEN_EXPIRES}" +%s)

  TOKEN_REMAINING=$(( (TOKEN_EXPIRES - NOW) / 60 ))

  if [ "$NOW" -ge "$TOKEN_EXPIRES" ] || [ "$TOKEN_REMAINING" -le 1 ]; then
    echo "🔄 $(date '+%H:%M:%S') Access token hết hạn, refreshing..."
    bash "$SCRIPT_DIR/gql-refresh-token.sh" && \
      echo "✅ $(date '+%H:%M:%S') Refreshed."
  else
    echo "⏳ $(date '+%H:%M:%S') Access token còn ~${TOKEN_REMAINING}p"
  fi

  sleep 30
done

Workflow hàng ngày

bash
# Sáng mở máy — chạy 1 lần
./scripts/gql-login.sh

# Để terminal này chạy ngầm suốt ngày
./scripts/gql-watch-token.sh

# IntelliJ tự có token mới, execute query bình thường

Hoặc dùng alias:

bash
# ~/.zshrc
alias gql-start="cd ~/projects/backops && bash ./scripts/gql-login.sh && bash ./scripts/gql-watch-token.sh"
bash
gql-start

Lưu ý

  • gql-watch-token.sh check mỗi 30 giây — đủ nhanh với access token TTL 5 phút
  • Mỗi lần refresh, server trả về refresh token mới (token rotation) — script tự update .env.local
  • Nếu idle quá 30 phút, refresh token hết hạn → watch script sẽ báo và exit → cần gql-login.sh lại
  • Timestamp so sánh luôn dùng UTC để tránh lệch timezone

Today I Learned