ASVS 5.0 V10.7.1 (Level 2) says the authorization server must ensure that the user consents to each authorization request. When the identity of the client cannot be assured, the server must always show an explicit consent prompt.
Remembering a past approval is allowed for clients you can trust. The rule is about not treating "approved once" as "approved forever for anything that claims to be this app".
Why it matters
Consent is the point where a human decides to give an application access to their data. Skip it, and a malicious or compromised client can obtain tokens just by getting a logged in user to open a link. This is the core of consent phishing.
Client identity is the hard part. A client ID and a display name are public strings, and anyone can copy them. Redirect URIs registered by the owner and strong client authentication are real signals. Where the client is public, dynamically registered or unverified, the safe default is to prompt every time.
How to implement it
Decide per request whether prior consent can be reused. A conservative rule looks like this:
def needs_consent_prompt(user, client, requested_scopes, request):
if request.params.get("prompt") == "consent":
return True
if not client.identity_assured:
return True # public, dynamic or unverified client
prior = db.get_consent(user.id, client.id)
if prior is None:
return True
if not set(requested_scopes) <= set(prior.scopes):
return True # new scopes need a new decision
if prior.expires_at <= datetime.now(timezone.utc):
return True
return False
Define what "identity assured" means in your system. Examples: a first party app owned by you, a client registered by an administrator with exact redirect URIs, or a client that authenticates with a key. A client that registered itself through open dynamic registration is never in that group.
Protect the consent form itself:
- Bind the consent submission to the user's session and to the exact pending request with a CSRF token and a request identifier.
- Do not let the consent page be framed by other origins. Send
Content-Security-Policy: frame-ancestors 'none'andX-Frame-Options: DENYso a hidden iframe cannot trick a click. - Store the decision server side with the scopes granted and a timestamp.
Consent also matters for tools that act for users. AI assistants and MCP servers often request access through OAuth. Safeguard's MCP guardrail demo shows how policy can sit between an agent and the tools it calls:
Click through it at your own pace, or open it full screen.
Prompt parameters
Honor the prompt parameter from OpenID Connect carefully. prompt=none means the client wants no user interface, so the server must answer with consent_required or login_required rather than showing a screen or granting silently. prompt=consent means the user must see the screen even if a prior grant exists.
Do not let a client downgrade the prompt for other clients. Consent records belong to the pair of user and client, never to the user alone.
Revocation of remembered consent
Give remembered consents an expiry, for example 30 to 90 days for lower risk scopes, and shorter for sensitive ones. When a client changes its registered redirect URIs or its owner, clear existing consents for it, because the thing the user approved is no longer the same thing.
How to test it
- Sign in as a user who has never authorized a test client. Start a flow and confirm a consent screen appears.
- Repeat with the same client and the same scopes. For a trusted first party client you may skip the prompt. For an unverified client, the prompt must appear again.
- Request an extra scope from a client that was approved before. A new prompt must show the added scope.
- Add
prompt=noneto the request for a client that needs consent. Expect an error such asconsent_required, not a silent grant. - Load the consent URL inside an iframe on another origin. The browser should refuse to render it.
- Post the consent form without a valid CSRF token, or from another session. Expect rejection.
Automating the check
Cover the prompt logic with unit tests on the decision function, then add two end to end checks: one for prompt=none returning an error, and one for the consent page returning framing protection headers.
curl -sI https://auth.example.com/consent | grep -iE "x-frame-options|content-security-policy"
Handling first party clients
Many teams have first party apps that share a login with the authorization server, and showing consent for them would annoy users with no gain. The requirement allows skipping the prompt when client identity is assured. Make that an administrator flag on the client record, record who set it and when, and review the list on a schedule. Treat any change to its redirect URIs as a trigger to review the flag.
Logging consent decisions
Record every consent decision with the user, the client, the scopes shown, the scopes granted and the time. When someone later asks why an app had access, you have an answer. These records also feed the review page in V10.7.3, where users see and revoke what they approved.
Common mistakes
- Auto approving all clients on a corporate tenant because users are "internal". Internal users still get phished.
- Trusting the client name or logo as proof of identity.
- Storing consent per user but not per client and scope.
- Allowing
prompt=noneto bypass a required decision. - Consent screens that can be clickjacked.
Quick check
When must the server always prompt?
Whenever the identity of the client cannot be assured, such as public, dynamically registered or unverified clients.
What should happen if a client asks for a new scope later?
The user should be asked again, because the earlier decision did not cover it.