webdevpuneet.com

OAuth Consent Screen with Granular Permission Scopes — Grant Exactly What You Mean To

OAuth Consent Screen — Granular Permission Scopes · Forms · Plain HTML, CSS & JS · Live preview

CategoryForms

Copy & paste

Source Code

<div class="demo">
  <div class="consent-card">
    <div class="consent-app">
      <div class="consent-app-icon">📋</div>
      <div>
        <p class="consent-app-name">TaskFlow</p>
        <p class="consent-app-desc">wants to access your Nimbus account</p>
      </div>
    </div>

    <div class="consent-account">
      <span class="consent-avatar">DW</span>
      <span>[email protected]</span>
    </div>

    <p class="consent-section-title">This will allow TaskFlow to:</p>
    <ul class="scope-list" id="scopeList">
      <li class="scope-item required">
        <label>
          <input type="checkbox" checked disabled />
          <div>
            <span class="scope-name">View your basic profile info</span>
            <span class="scope-desc">Name, email address, and avatar. Required — cannot be disabled.</span>
          </div>
        </label>
      </li>
      <li class="scope-item">
        <label>
          <input type="checkbox" data-scope="calendar" checked />
          <div>
            <span class="scope-name">Read your calendar events</span>
            <span class="scope-desc">See event titles, times, and attendees on your primary calendar.</span>
          </div>
        </label>
      </li>
      <li class="scope-item">
        <label>
          <input type="checkbox" data-scope="contacts" checked />
          <div>
            <span class="scope-name">Read your contacts</span>
            <span class="scope-desc">See names and email addresses in your contact list.</span>
          </div>
        </label>
      </li>
      <li class="scope-item risky">
        <label>
          <input type="checkbox" data-scope="send-email" />
          <div>
            <span class="scope-name">Send email on your behalf</span>
            <span class="scope-desc">Send messages from your account without further review. Sensitive permission.</span>
          </div>
        </label>
      </li>
    </ul>

    <div class="consent-actions">
      <button class="btn ghost" id="denyBtn">Deny</button>
      <button class="btn primary" id="allowBtn">Allow selected</button>
    </div>
    <p class="consent-footnote" id="consentFootnote">2 optional permissions selected, plus required profile access.</p>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Each optional permission scope can be individually toggled on or off before granting, not an all-or-nothing bundle
Required scope (basic profile) is visually and functionally distinct — locked checked, disabled, clearly labeled as non-optional
Sensitive/high-risk scope is visually flagged with distinct styling and explicit risk-level copy, not blended in with low-risk scopes
Allow button's own label live-updates to state exactly how many permissions are about to be granted
Footnote summary text reflects the current live selection, removing any ambiguity about what will actually be authorized
Granted-scopes payload is read from live checkbox state at the exact moment of the click, never a stale cached selection
Deny path is fully separate and always available, granting nothing regardless of the optional scope selections

About this UI Snippet

Granular OAuth Consent — Letting Users Grant Exactly the Permissions They Mean To

Screenshot of the OAuth Consent Screen — Granular Permission Scopes snippet rendered live

Most OAuth consent screens present a single all-or-nothing "Allow" button covering an entire bundle of requested permissions — accept the whole list, or deny the app access altogether. This snippet implements a more respectful pattern: each *optional* scope can be individually unchecked before granting, and only the one genuinely non-negotiable scope (basic profile info, needed simply to know who's authorizing) is locked as required.

Required versus optional scopes are visually and functionally distinct

The basic-profile-info checkbox is rendered checked disabled — visually similar to the other rows but unmistakably non-interactive (slightly dimmed, no pointer cursor on its label), with explicit copy stating it's required and cannot be disabled. Every other scope is a genuinely live, user-controllable checkbox. This distinction matters: conflating "things this app minimally needs to function" with "things this app would merely like to have access to" into one identical-looking list either forces users to accept more than they're comfortable with, or makes even the truly essential permission feel deniable when it isn't.

The sensitive scope is visually flagged, not just listed identically to the rest

The "Send email on your behalf" scope sits in a row styled with a distinct amber background and explicitly labeled "Sensitive permission" in its description — a deliberate visual escalation signaling this specific permission carries meaningfully more risk than read-only calendar or contacts access. Real OAuth providers (Google's consent screen among them) make exactly this kind of severity distinction, because burying a genuinely risky permission in a visually uniform list of low-risk ones makes it easy for a user to grant something they wouldn't have if it had been called out.

The Allow button's own label reflects the live selection, not a static caption

updateSummary() runs on every checkbox change and rewrites both the footnote text and the Allow button's own label to state exactly what's about to be granted — "Allow 2 selected" or "Allow (profile only)" if every optional scope has been unchecked. This is a deliberate anti-ambiguity measure: a user should never have to infer what clicking Allow will actually authorize from a static, unchanging button caption while the checkboxes above it have changed.

The granted-scopes payload is read live at the moment of the click

The click handler builds its grantedScopes array by filtering the checkboxes for their *current* checked state at the exact moment Allow is clicked — not from some earlier cached selection. This guarantees the authorization request sent (in a real implementation, to the actual OAuth backend) always matches precisely what the user was looking at and had selected the instant they committed to it.

Step by step

How to Use

  1. 1
    Review the requesting app and accountThe app's name, icon, and a short description of what it wants sit at the top, followed by which account is authorizing it.
  2. 2
    Uncheck any optional scope you don't want to grantCalendar, contacts, and email-sending access are all individually toggleable — only the basic profile scope is locked as required.
  3. 3
    Notice the sensitive scope's distinct stylingThe "Send email on your behalf" row is visually flagged with an amber background and explicit "Sensitive permission" copy, unchecked by default.
  4. 4
    Watch the footnote and Allow button update liveBoth reflect exactly how many optional permissions are currently selected, so there's no ambiguity about what clicking Allow will authorize.
  5. 5
    Click Allow or DenyAllow grants exactly the currently-checked scopes (logged to the console in this demo); Deny grants nothing at all.

Real-world uses

Common Use Cases

AUTH
Third-party app authorization flows
The exact real-world context this pattern is modeled on — an OAuth-style "app wants access to your account" consent screen.
INTEGRATION
Integration marketplace permission grants
Platforms with an integrations marketplace (Slack apps, browser extensions) benefit from the same granular, individually-toggleable permission model.
PRIVACY
Privacy-respecting permission requests
Any flow requesting multiple distinct permissions at once benefits from letting users grant a subset rather than forcing an all-or-nothing choice.
ADMIN
Internal tool access request approval
Internal admin tools requesting elevated access to specific resources can use the same granular scope-review pattern for an approver to selectively grant.
Related: Accordion — CSS Only Checkbox Hack (No JavaScript)
See the Accordion — CSS Only Checkbox Hack (No JavaScript) for a related misc pattern worth pairing with this one.
Related: Share-URL State Sync — Filters Encoded into a Copyable Link
See the Share-URL State Sync — Filters Encoded into a Copyable Link for a related misc pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Basic profile information (name, email, avatar) is the one piece of access genuinely required for the requesting app to function at all — it needs to know who is authorizing it. Every other scope represents optional, more specific access that a user might reasonably want to withhold while still using the app in a reduced capacity.

It sits in a row with a distinct amber background and explicit "Sensitive permission" copy in its description, and is unchecked by default — a deliberate visual escalation to make sure a user notices and consciously decides on this specific higher-risk permission rather than granting it by default alongside lower-risk scopes.

The Allow button label updates to read "Allow (profile only)" and the footnote confirms only required profile access will be granted — clicking it in that state authorizes just the one required scope and none of the optional ones.

It reflects your current, live selection at the exact moment you click Allow — the click handler reads each checkbox's actual checked state at that instant, not any earlier default or cached selection.

Yes — the Deny button is completely independent of the checkbox selections and always grants nothing, regardless of which optional scopes happen to be checked at the time.

Replace the console.log(grantedScopes) call inside the Allow click handler with an actual request to your OAuth authorization endpoint, sending the grantedScopes array (or your provider's equivalent scope string format) as the authorized permission set.