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.
| Scope | Consent-screen wording | Grants | Sensitive | Self-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 slice | yes | ❌ |
civic:activity | ”Your civic activity — representatives you have contacted and what you urged” | A deployment data slice | yes | ❌ |
Three filters, in this order
- Parse.
ParseScopessplits the space-delimited string and drops anything not inSupportedScopes. 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. - Client ceiling.
FilterScopesintersects the request with the client’sallowed_scopes, so a compromised client cannot escalate its own access by asking for more. - 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_accessis 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.
Read next
- The grants — where the granted scope set ends up.
- Client properties →
allowed_scopes - OIDC provider → the
id_token