Skip to content
rw3iss Auth

Scopes

Scopes are declared in internal/auth/oidc/token.go. SupportedScopes is both what discovery advertises and the allow-list an authorization request is filtered through.

ScopeConsent-screen wordingGrantsSensitiveSelf-service
openid”Confirm who you are”Required on every request. Without it the authorize endpoint fails invalid_scope.✅
profile”Your name, username and picture”name, given_name, family_name, preferred_username, picture, cg_roles in the id_token✅
email”Your email address”email, email_verified in the id_token✅
offline_access”Stay signed in when you are not using the site”A refresh token, when the client also has the refresh_token grant❌
civic:location”Your city, state and country (never your street address)“A deployment data slice❌
civic:interests”The civic topics you follow”A deployment data slice❌
civic:positions”The positions you have declared on issues”A deployment data sliceyes❌
civic:activity”Your civic activity — representatives you have contacted and what you urged”A deployment data sliceyes❌

Three filters, in this order

  1. Parse. ParseScopes splits the space-delimited string and drops anything not in SupportedScopes. Dropping rather than erroring follows RFC 6749 §3.3 — the server MAY ignore scopes it does not recognise. The granted set is echoed in the token response, so a client can always see exactly what it received.
  2. Client ceiling. FilterScopes intersects the request with the client’s allowed_scopes, so a compromised client cannot escalate its own access by asking for more.
  3. Consent. Anything not already granted by this person to this client triggers the consent screen — unless the client is trusted.

openid is checked after all three: if it did not survive, the request fails with invalid_scope, “the openid scope is required”.

Claims are filtered by scope

if HasScope(in.Scopes, ScopeEmail) { claims.Email = …; claims.EmailVerified = … }
if HasScope(in.Scopes, ScopeProfile) { claims.Name = …; …; claims.Roles = … }

A client that asked only for openid gets a subject and nothing else — not the email, not the name. That is the difference between a consent screen that means something and one that is decoration.

cg_app_code and cg_namespaces are set unconditionally when the client has an app_code; they are not gated on a scope.

The two sensitive scopes

civic:positions and civic:activity are in SensitiveScopes, and they behave differently in exactly one way: they always re-prompt.

// missingScopes()
if oidc.SensitiveScopes[w] {
out = append(out, w) // always "missing", whatever was granted before
continue
}

A standing consent covers the ordinary scopes. It never covers these. Consent for political-belief data should be a decision the person remembers making, not one inherited from months ago.

They are also rendered with a visible — sensitive marker on the consent screen.

The reasoning for keeping them out of profile is the same: bundling them into a broad “profile” scope would mean a relying party asking for a display name silently receives someone’s political record. They are separate so consent can be separate.

The self-service restriction

var SelfServiceScopes = []string{ScopeOpenID, ScopeProfile, ScopeEmail}

A client registered through POST /oidc/clients may request identity only. Two separate reasons:

  • The civic:* scopes read data a member has not agreed to hand to a stranger. They are granted by an administrator, after a human decision.
  • offline_access is excluded for a related reason — a refresh token is standing, long-lived access, which is not something a registration form should be able to award itself.

An out-of-range scope is rejected, not silently dropped. A silent drop leaves the developer with a client that authorises fine and then returns a token missing the claim they asked for, with nothing anywhere explaining why — the kind of failure that costs a day.

{"error":{"code":"INVALID_INPUT","details":{"field":"allowed_scopes"},
"message":"A self-registered application may request only these scopes: openid, profile, email. \"civic:positions\" is granted by a CivicGate administrator — contact us if your application needs it."}}

openid is always force-included in the resulting list, even if you omit it.

What is not enforced today

Documented because assuming otherwise will mislead you.