GOCOSYS Account Docs

Tokens & refresh

What each token is for, how long it lasts, and how to verify it.

The three tokens

TokenLifetimeUse
Authorization code60 seconds, single useExchanged once for tokens.
Access token15 minutes (900s)Calling /oauth/userinfo.
Refresh token30 daysGetting a new access token without re-prompting.
ID tokenA signed assertion of who signed in. Not a credential.

The access token is deliberately narrow

It carries an audience of your client_id and is accepted only at /oauth/userinfo. It is not a session token, it carries no roles, and it grants nothing on the /api/** surface. Do not attempt to call other endpoints with it.

Verifying the ID token

Signed with HS256 — a symmetric algorithm — using your client secret as the key. Verify:

  • Signature, with your client_secret as the HMAC key.
  • iss equals https://account.gocosys.com.
  • aud equals your client_id.
  • exp is in the future.
  • nonce matches the one you sent, if you sent one.

JWKS will not help you. /oauth/jwks.json returns an empty key set, because a symmetric key cannot be published without giving away the ability to forge tokens. Libraries that insist on fetching keys from JWKS must be configured with the client secret instead — this is the single most common integration failure here.

Worked example — Python

With PyJWT the whole verification is one call. Pin the algorithm to HS256; never feed the token's own alg back in, and never use options={"verify_signature": False}.

import jwt

claims = jwt.decode(
    id_token,
    YOUR_CLIENT_SECRET,          # the HMAC key
    algorithms=["HS256"],        # pinned, never read from the token header
    audience=YOUR_CLIENT_ID,
    issuer="https://account.gocosys.com",
)

Worked example — Django

For mozilla-django-oidc, the two settings that matter are the signing algorithm and the secret used as the HMAC key:

OIDC_RP_CLIENT_ID     = "your_client_id"
OIDC_RP_CLIENT_SECRET = "your_client_secret"
OIDC_RP_SIGN_ALGO     = "HS256"      # must match discovery; do NOT set RS256

OIDC_OP_AUTHORIZATION_ENDPOINT = "https://account.gocosys.com/oauth-authorize"
OIDC_OP_TOKEN_ENDPOINT         = "https://account.gocosys.com/oauth/token"
OIDC_OP_USER_ENDPOINT          = "https://account.gocosys.com/oauth/userinfo"

OIDC_USE_PKCE                   = True
OIDC_PKCE_CODE_CHALLENGE_METHOD = "S256"
OIDC_RP_SCOPES                  = "openid profile email"

Leave OIDC_OP_JWKS_ENDPOINT unset. With OIDC_RP_SIGN_ALGO = "HS256" the library uses OIDC_RP_CLIENT_SECRET as the key and never needs JWKS; pointing it at our empty key set is exactly what produces the "no keys found" failure.

Refreshing

curl -X POST https://account.gocosys.com/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token=THE_REFRESH_TOKEN \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET

Refresh when the access token is near expiry, or when a call returns 401. If the refresh itself fails, the grant is gone — send the user back through authorization.

Tokens can die early

Do not assume a token survives to its stated expiry. It stops working when:

  • the user revokes your application from their Connected apps page;
  • an administrator revokes the client;
  • you revoke it yourself.

Treat 401 as "re-authorize this user", not as a transient error to retry. Retrying a revoked token in a loop will never succeed.

Revoking

curl -X POST https://account.gocosys.com/oauth/revoke \
  -d token=ACCESS_OR_REFRESH_TOKEN \
  -d token_type_hint=refresh_token \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET

Call this when a user signs out of your application, if you do not intend to keep acting on their behalf. It always returns 200, even for an unknown token.

Storing tokens

  • Keep refresh tokens server-side, encrypted at rest. They are long-lived credentials.
  • Never put tokens in localStorage in a browser — any XSS reads them.
  • Never log them. They grant access on their own.
  • Scope them to the user they belong to, so one account's token can never be used for another.