UpWork OAuth2 doesn't refresh tokens

I have similar issue to this one still appearing to me. My graphql node works just fine for exactly 24 hours and then it through Authentication failed error. I manually reconnect the oauth2 credentials and it keeps working for another 24 hours.

Is there any fix to that? I don’t want to manually handle refresh tokens in my database.

Struggling with the same issue - can anyone help here

I think it’s something from Upwork side:

Upwork API uses OAuth 2.0 for secure authentication, following RFC 6749 standards. Request tokens expire in 24 hours, but access tokens do not expire and can be refreshed biweekly. Each application requires unique client credentials. Developers must log in to request these credentials and consult the API documentation for full details.

Note that OAuth Request tokens expire in 24 hours. You can refresh the token every two weeks or less. Once the Access token is created, it never expires.

https://support.upwork.com/hc/en-us/articles/115015933448-API-authentication-and-security

1 Like

There’s no such thing as access token that never expires on upwork (unless I missed something). I spoke to others, pretty much everyone does custom handling of refresh tokens. But I wonder if there’s more elegant way of doing it or if n8n can handle refresh tokens itself.

A side question, by the way:
I’m also on Upwork, but I always hear that it’s better to avoid using the Upwork API because if any issue happens, it might put your account at risk of suspension. Is that true?

Haven’t heard about any issues regarding the account suspension. I am using an API for quite some time without any problems.

In terms of a solution, I ended up creating a sub-workflow which stores access and refresh key in the database. Every 12 hours it refreshes the token.

2 Likes
I created my own application using python, mysql, graphql, and cronjobs to pull Upwork data, store in DB, then generate RSS feed to display what I want. I fire off a cronjob to automatically refresh the token and will even send me an email if it fails for any reason. Let me know if I can help.

#!/usr/bin/env python3
import os
import json
from datetime import datetime, timedelta
from requests_oauthlib import OAuth2Session
import smtplib
from email.mime.text import MIMEText
from pathlib import Path
import tempfile

# ----- Paths (env overrides -> secure defaults) -----
CONFIG_PATH = Path(os.getenv("CONFIG_PATH", "/var/secure/upwork_feed_secrets/config.json"))
TOKEN_PATH  = Path(os.getenv("TOKEN_PATH",  "/var/secure/upwork_feed_secrets/token.json"))
ERROR_LOG_PATH = Path("/tmp/upwork_token_error.log")  # temp file to track last alert date

# ----- Helpers -----
def load_json(path: Path):
    if path.exists():
        with path.open("r") as f:
            return json.load(f)
    return None

def save_json_atomic(path: Path, data: dict):
    # write to a temp file then move into place
    with tempfile.NamedTemporaryFile("w", delete=False, dir=str(path.parent)) as tmp:
        json.dump(data, tmp, indent=2)
        tmp.flush()
        os.fsync(tmp.fileno())
        tmp_name = tmp.name
    os.replace(tmp_name, path)

def now_ts():
    return datetime.now()

def parse_port(port_value, default=587):
    try:
        return int(port_value)
    except Exception:
        return default

def get_smtp_config(cfg: dict):
    """
    Support either:
      cfg["smtp2go"] = {username, password, smtp_server, smtp_port, (optional) from, to}
      cfg["smtp"]    = {username, password, host, port, from, to}
    Returns normalized dict or None.
    """
    smtp = cfg.get("smtp")
    s2g  = cfg.get("smtp2go")
    if smtp:
        return {
            "host": smtp.get("host"),
            "port": parse_port(smtp.get("port", 587)),
            "username": smtp.get("username"),
            "password": smtp.get("password"),
            "from": smtp.get("from") or smtp.get("username"),
            "to": smtp.get("to"),
        }
    if s2g:
        return {
            "host": s2g.get("smtp_server"),
            "port": parse_port(s2g.get("smtp_port", 587)),
            "username": s2g.get("username"),
            "password": s2g.get("password"),
            "from": s2g.get("from") or s2g.get("username"),
            "to": s2g.get("to") or "[email protected]",  # default you used in original
        }
    return None

def should_send_email():
    if ERROR_LOG_PATH.exists():
        try:
            with ERROR_LOG_PATH.open("r") as f:
                last_sent = datetime.strptime(f.read().strip(), "%Y-%m-%d")
                return last_sent.date() < now_ts().date()
        except Exception:
            return True
    return True

def mark_email_sent():
    with ERROR_LOG_PATH.open("w") as f:
        f.write(now_ts().strftime("%Y-%m-%d"))

def send_alert_email(smtp_cfg: dict | None, error_msg: str):
    if not smtp_cfg:
        # silently skip if no SMTP configured
        print("[WARN] SMTP not configured; skipping alert.")
        return
    try:
        msg = MIMEText(f"❌ Upwork token refresh failed:\n\n{error_msg}")
        msg["Subject"] = "Upwork Token Refresh Failed"
        msg["From"] = smtp_cfg["from"]
        msg["To"] = smtp_cfg["to"]

        with smtplib.SMTP(smtp_cfg["host"], smtp_cfg["port"]) as server:
            server.starttls()
            server.login(smtp_cfg["username"], smtp_cfg["password"])
            server.sendmail(smtp_cfg["from"], [smtp_cfg["to"]], msg.as_string())
            print("🚨 Alert email sent.")
    except Exception as e:
        print(f"[WARN] Failed to send email: {str(e)}")

def need_refresh(token: dict) -> bool:
    """
    If expires_at exists: refresh when we're within 60s of expiry.
    If not present: play it safe and refresh.
    """
    try:
        exp_at = token.get("expires_at")
        if exp_at is None:
            return True
        expires_at = datetime.fromtimestamp(exp_at)
        return now_ts() >= (expires_at - timedelta(seconds=60))
    except Exception:
        return True

def main():
    # Load config
    cfg = load_json(CONFIG_PATH)
    if not cfg:
        print(f"[ERROR] No config found at {CONFIG_PATH}")
        return

    # Upwork creds (your current keys)
    upwork = cfg.get("upwork") or {}
    client_id = upwork.get("api_key") or upwork.get("client_id")
    client_secret = upwork.get("api_secret") or upwork.get("client_secret")
    redirect_uri = upwork.get("redirect_uri")

    if not client_id or not client_secret:
        msg = "Missing upwork.api_key/api_secret (or client_id/client_secret) in config.json"
        print(f"[ERROR] {msg}")
        if should_send_email():
            send_alert_email(get_smtp_config(cfg), msg)
            mark_email_sent()
        return

    # SMTP config (optional)
    smtp_cfg = get_smtp_config(cfg)

    # Load token
    token = load_json(TOKEN_PATH)
    if not token:
        print("[ERROR] No token.json found.")
        if should_send_email():
            send_alert_email(smtp_cfg, "token.json missing")
            mark_email_sent()
        return

    # Prepare OAuth session
    token_url = "https://www.upwork.com/api/v3/oauth2/token"

    try:
        if need_refresh(token):
            print("🔄 Token needs refresh. Refreshing...")
            # Upwork expects client_id & client_secret in the body for refresh
            oauth = OAuth2Session(client_id, token=token, redirect_uri=redirect_uri,
                                  auto_refresh_url=token_url, auto_refresh_kwargs={
                                      "client_id": client_id,
                                      "client_secret": client_secret,
                                  }, token_updater=lambda t: None)  # we save manually below

            # Explicit call to refresh endpoint
            new_token = oauth.refresh_token(
                token_url,
                client_id=client_id,
                client_secret=client_secret,
            )

            # Persist atomically
            save_json_atomic(TOKEN_PATH, new_token)
            print("✅ Token refreshed and saved.")
        else:
            print("✅ Token is still valid.")
    except Exception as e:
        err = str(e)
        print(f"❌ Token refresh failed: {err}")
        if should_send_email():
            send_alert_email(smtp_cfg, err)
            mark_email_sent()

if __name__ == "__main__":
    main()

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.