8. Choose scopes and storage deliberately
A working OAuth flow is not automatically a safe OAuth flow. Safety comes from two decisions you make before and after the token arrives: request only the permission the feature needs, and store the resulting token according to the risk of the application.
Least privilege is a product decision
Least privilege means asking for the smallest permission that supports the feature the user is choosing right now. It is not just a security slogan. It changes how trustworthy your app feels.
If the app only needs to show the signed-in user's GitHub username, read:user is enough. If it needs to inspect public repositories, move to a repository scope. If it needs private repositories, explain why before asking for broader access.
GitHub repository scopes are not tiny
The current GitHub OAuth App scope model is coarse compared with GitHub Apps. In particular, repo is broad. It is not merely "read private repositories." It grants wide access to public and private repository resources for repositories the user can access.
If your app asks for repo, the user is being asked to trust you with private repository access. Do not request it just because the endpoint you want is convenient. Make the feature earn that permission.
Progressive authorization
Progressive authorization means starting with a small scope, then asking for more only when the user chooses a feature that needs it.
- First connection: request
read:userso the app can identify the authorized account. - Public repository feature: ask for the narrowest repository permission that supports that feature.
- Private repository feature: explain the value, then ask for broader access only if the user chooses that feature.
This design keeps the first OAuth flow less intimidating and makes later permission upgrades easier to justify.
Progressive authorization does not make GitHub OAuth App scopes fine-grained. If your product needs per-repository installation choices or narrower repository permissions, that is a signal to evaluate GitHub Apps rather than stretching OAuth App scopes beyond what they were designed to express.
Storage mistakes to avoid
Once a token exists, storage decisions determine how widely a mistake can spread. Avoid these shortcuts when a demo becomes a real project:
| Bad habit | Why it is dangerous |
|---|---|
| Hardcode tokens in source | The token can be committed, copied, indexed, or reused by anyone who sees the file. |
| Print full tokens | Terminal history, screenshots, logs, and support transcripts become credential storage. |
| Store tokens in plain text forever | A stolen laptop, backup, or project folder becomes an account-access problem. |
| Put long-lived tokens in browser storage | Any XSS bug can become a token theft bug. |
| Share tokens in chat or tickets | They become searchable, archived, and visible to more people than intended. |
Choose storage by risk
| Context | Reasonable starting point | What to avoid |
|---|---|---|
| Learning script | Memory, or a gitignored .env file for a short local test. |
Committing tokens or printing full tokens. |
| Local CLI | OS credential storage or another local secret store when available. | Plain text token files that linger forever. |
| Web app | Server-side storage, encrypted at rest, with secrets managed separately. | Putting long-lived access tokens in browser storage. |
| Production service | Secrets manager, encrypted database fields, audit logs, rotation plan. | Sharing secrets through chat, tickets, screenshots, or logs. |
Mask what you log
Sometimes you need to know whether a token exists or whether a refresh happened. You almost never need the whole token in a log. The mask_token() helper from the previous scripts is the right pattern: show just enough to debug which credential was used, never enough to reuse it.
Storage and scope choices determine how much damage a mistake can do. Token lifetime is the next layer: what happens when tokens expire, rotate, or get revoked.