ASVS 5.0 V7.2.2 (Level 1) requires that user sessions are managed with self contained or reference tokens that are generated dynamically for each session. A static API secret or key that stays the same from day to day is not a session token, and it must not be used as one.
Why it matters
A static key has no lifecycle. It does not expire when the user logs out, it cannot be tied to one sign in, and it often lands in places tokens should not: source code, mobile app bundles, shell history, support tickets. Anyone who copies it holds permanent access.
A dynamic token is born at authentication, carries a limited lifetime, can be revoked on its own, and tells you which sign in produced it. When one leaks, the blast radius is one session.
This is different from machine to machine credentials. Services sometimes do need long lived secrets. The requirement is about representing a user session. For service accounts, prefer short lived credentials issued by an identity system, and keep unavoidable static secrets in a secrets manager with rotation.
How to implement it
Issue a fresh token after successful authentication, with an expiry, and tie it to the user and the sign in event.
import secrets, hashlib, datetime as dt
def issue_session(db, user_id, lifetime_hours=12):
raw = secrets.token_urlsafe(32)
db.execute(
"INSERT INTO sessions (token_hash, user_id, created_at, expires_at) "
"VALUES (?, ?, ?, ?)",
(
hashlib.sha256(raw.encode()).hexdigest(),
user_id,
dt.datetime.utcnow(),
dt.datetime.utcnow() + dt.timedelta(hours=lifetime_hours),
),
)
return raw # sent once in an HttpOnly, Secure cookie
For signed tokens, give each one a unique jti, a short exp and an audience. Use refresh tokens with rotation if you need longer sessions.
Where you offer API keys to developers for scripting, label them as personal access tokens, make them expire, scope them narrowly, and let users list and revoke them. They are a different credential type with their own rules, not a replacement for a session.
Then take away the temptation: remove hard coded keys from clients and repositories. Scanning for committed secrets finds the ones that slipped in.
How to test it
- Log in twice as the same user. The two tokens must differ.
- Log out and replay the old token. It must fail.
- Look for any endpoint that accepts a constant value, an environment variable value or a shared header as proof of user identity.
- Search the repositories for keys in config files, mobile apps and front end bundles. The free in-browser dependency scan at https://gold.safeguard.sh/scan covers your dependency tree, and the secrets scanning demo below shows what committed key detection looks like.
- Check that no token has an exp far in the future (years, or none at all).
Click through it at your own pace, or open it full screen.
Common mistakes
- Treating an admin API key as "the session" for an internal console.
- Embedding one shared key in a mobile app and calling the user logged in.
- Issuing tokens with no expiry so they behave like keys.
- Reusing the same value for every user of an integration.
- Putting the key in a URL, where it lands in logs.
Try it yourself
Pick one non human integration in your system and list the credential it uses. Ask three questions: how does it expire, how do I revoke only it, and who can see it. If any answer is "it doesn't", move it to a short lived token flow.
Quick check
Are personal access tokens a violation?
No, provided they are generated per user, scoped, expiring and revocable, and not used as the browser session itself.
Does a long lived JWT satisfy the requirement?
It is dynamically generated, but a token that never expires behaves like a static key. Keep lifetimes short and enforce V7.3.2.