Some time has passed since Part 1, 2 and 3 of this series.
That also means that Service Accounts that were created a while ago may run into token expiration sooner rather than later. Thankfully, Atlassian has provided new tools that make managing Service Accounts a lot easier on a larger scale, so let's jump right into it.
β¦gotta love Atlassian terminology and naming π https://developer.atlassian.com/cloud/admin/api-access/rest/intro
The API Access API gives you primarily read access to API tokens and OAuth credentials in the organization. For Service Accounts, the interesting endpoints are currently:
π‘ These endpoints require an Organization API Key with certain scopes β after reading the article series you of course know how scopes work and where to find the required ones π π‘
Important limitation: As I am writing this post, the Endpoints around Service Accounts and Tokens are still experimental and are blocked per default. Your Organization needs to be added to a whitelist through Support if you want to use them.
The API will help you find and revoke tokens. It does not let you create new Service Account tokens automatically. Token renewal still needs a manual step in the Admin UI.
While Service Accounts can live for all eternity, Tokens have a maximum lifespan of 365 days. As an Org Admin, you will get an Email for Token expiration. Service Account Owners are not technically linked to their Service Accounts, so (worst case) their Automations and Integrations will fail silently β unless you act proactively.
π‘ We only need to worry about API Tokens, as OAuth Credentials don't have expiration dates π‘
So how to find those expiring tokens?
The idea is simple:
expiresAt Date is less than x days awayπ‘ Do not wait until the last day. Tokens are credentials, and credential changes always have the potential to break something. Give yourself enough time to coordinate with the owning team. π‘
Thank god for Coding LLMs that grant me the ability to provide an example Python script for you to use and adjust π The report will return expiring tokens per Service Account and also includes a link directly to the correct UI in the administration.
"""
Finds Atlassian Cloud Service Account API tokens that are expiring soon.
Usage:
Set ORG_ID, API_KEY, MIN_DAYS_AWAY, and MAX_DAYS_AWAY below, then run:
python ExpiringTokens.py
"""
import re
import time
from datetime import datetime, timezone
import requests
# --- CONFIGURATION ---
ORG_ID = "xxx"
API_KEY = "xxx"
MIN_DAYS_AWAY = 0
MAX_DAYS_AWAY = 30
# --- END CONFIGURATION ---
BASE_URL = "https://api.atlassian.com"
MAX_RETRIES = 5
def request_with_retry(url, headers):
"""GET request that automatically retries on 429 rate limiting."""
for attempt in range(1, MAX_RETRIES + 1):
response = requests.get(url, headers=headers)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
wait_time = int(retry_after) if retry_after and retry_after.isdigit() else 2 ** attempt
print(f"Rate limited. Waiting {wait_time}s before retry ({attempt}/{MAX_RETRIES})...")
time.sleep(wait_time)
continue
if response.status_code >= 400:
print(f"Error {response.status_code} calling {url}")
print(f"Response body: {response.text}")
response.raise_for_status()
return response
raise RuntimeError(f"Gave up after {MAX_RETRIES} retries due to rate limiting: {url}")
def parse_next_link_header(link_header):
"""Parses an RFC 5988 Link header for a rel='next' URL, if present."""
if not link_header:
return None
for part in link_header.split(","):
match = re.match(r'\s*<([^>]+)>\s*;\s*rel="?next"?', part)
if match:
return match.group(1)
return None
def paginated_get(url, headers):
"""
Yields all items from an Atlassian admin API endpoint.
Handles both response shapes seen in these APIs:
- a bare JSON list (tokens endpoint)
- an object with items/links (accounts endpoint)
"""
while url:
response = request_with_retry(url, headers)
payload = response.json()
if isinstance(payload, list):
items = payload
next_link = parse_next_link_header(response.headers.get("Link"))
else:
items = payload.get("items", payload.get("data", []))
next_link = payload.get("links", {}).get("next")
yield from items
if not next_link:
url = None
elif next_link.startswith("http"):
url = next_link
else:
url = BASE_URL + next_link
def parse_date(value):
"""Parses an ISO 8601 timestamp string into a timezone-aware datetime."""
if not value:
return None
return datetime.fromisoformat(value.replace("Z", "+00:00"))
def main():
if ORG_ID == "your_org_id_here" or API_KEY == "your_api_key_here":
print("Error: Please set ORG_ID and API_KEY at the top of the script.")
return
headers = {
"Authorization": f"Bearer {API_KEY}",
"Accept": "application/json",
"X-ExperimentalApi": "opt-in",
}
today = datetime.now(timezone.utc)
found = 0
print("Fetching service accounts...")
accounts_url = f"{BASE_URL}/admin/api-access/v1/orgs/{ORG_ID}/service-accounts"
for account in paginated_get(accounts_url, headers):
account_id = account.get("id")
account_name = account.get("displayName", "N/A")
tokens_url = (
f"{BASE_URL}/admin/api-access/v1/orgs/{ORG_ID}"
f"/service-accounts/{account_id}/api-tokens"
)
try:
tokens = list(paginated_get(tokens_url, headers))
except requests.exceptions.HTTPError:
continue
for token in tokens:
expires_at = parse_date(token.get("expiresAt"))
if not expires_at:
continue
days_left = (expires_at - today).days
if MIN_DAYS_AWAY <= days_left <= MAX_DAYS_AWAY:
found += 1
print("-" * 60)
print(f"Service Account : {account_name} ({account_id})")
print(f"Token Label : {token.get('label', 'N/A')}")
print(f"Token Status : {token.get('status', 'N/A')}")
print(f"Created : {token.get('createdAt', 'N/A')}")
print(f"Expires : {token.get('expiresAt')} ({days_left} days left)")
print(f"Last Active : {token.get('lastActiveAt') or 'Never'}")
print(
f"Renewal Link : https://admin.atlassian.com/o/{ORG_ID}"
f"/service-accounts/{account_id}?tab=credentials"
)
print("-" * 60)
print(f"Done. {found} token(s) expiring between {MIN_DAYS_AWAY} and {MAX_DAYS_AWAY} days.")
if __name__ == "__main__":
main()
Once we have the overview of soon-to-expire tokens, we need to work on renewing those tokens. Unfortunately, this part is still not fully automatable. There is no public API for creating a new Service Account API token with scopes. So the renewal process still requires clicking in the UI.
π‘ Service Account Owners need time to exchange tokens in their scripts and integrations, so old and new tokens need to be active simultaneously for a while π‘
I'd recommend the following flow:
Token expiration is only one part of the story. The second question is: "Do we still need this Service Account at all?"
The API token response contains lastActiveAt. That means you can identify tokens that have not been used for a while and then review the owning Service Account. You can use the script from above or slightly adjust it to filter for lastActiveAt Dates instead of expiresAt.
Remember that there could be active OAuth Credentials lurking somewhere, so always double check with the Service Account Owner, before deleting anything. Revoking credentials could be a first step. Deleting the Service Account will also remove all its space permissions and it's tedious to restore all permissions if they needed the Account after all.
Before removing a Service Account:
Atlassian has provided some new tooling to help us manage Service Accounts and their credentials. While parts remain manual, this also gives us the chance for a regular check if everything is still active and needed.
My recommendation: build a lightweight monthly review using the script as a little helper. Find tokens that expire soon, renew them with the owning team, and use the same report to identify accounts that may no longer be needed. If you have mandatory Service Requests for Service Account / Token creation, you could use the same channel for Token renewals, approvals for deletion etc.
π‘ Ideally, on Token creation, you're already setting the expiration dates on a specific day of the month (e.g. the 1st). That way, you can find Tokens that are about to expire at the beginning of the Month, Account owners will have enough time to update their scripts and integrations, and before the next expiration run, you can just remove all expired tokens. Make it a scheduled job once a month, rather than being triggered per expiring token π‘
I'm interested: How are you currently tracking Service Accounts and token renewals? Let me know in the comments and I'll add useful ideas to this article.
Note: I've updated relevant sections in Part 1, 2 and 3 to reflect these changes as well.
Rebekka Heilmann _viadee_
2 comments