The first version of Noomachy DevTracker signed users in with Firebase Auth's GitHub provider and asked for the repo scope. That worked, and it was wrong in two ways that took a while to fully appreciate.
The scope problem
GitHub's OAuth repo scope is not "read some repositories". It is full read and write access to every private repository the user can reach. Every one. Forever, until revoked.
For a product that reads commit messages and file paths in order to write social posts, that is an enormous ask. Worse, it is an ask the user has to accept in full or not use the product. There is no middle setting.
Put yourself in the position of someone evaluating a new tool. A consent screen that says this app can read and write all your private repositories, in exchange for generating LinkedIn posts, is a reasonable place to close the tab.
The token problem
The second issue is where the token ends up.
Firebase Auth hands the OAuth access token to the browser by design. That is how the client SDK works. Our first version then stored it in localStorage so the app could make GitHub calls from the front end.
A repo-scoped token in localStorage is readable by any cross-site scripting bug, any malicious browser extension, and any script that ends up on the page. Long lived, broadly scoped, and sitting in the most accessible storage the browser offers.
Critically, this could not be fixed by moving the token server-side after login. Firebase gives the token to the client as part of the flow. There is always a window, and a design that depends on a race being won is not a design.
What a GitHub App changes
A GitHub App is a different primitive, and it fixes both problems at once.
Access is per-repository. During installation the user picks exactly which repositories the app can see. Not a scope, a list. Someone can grant two side projects and withhold everything belonging to their employer, which is what most developers actually want.
Permissions are fine grained. Instead of repo, the app declares what it needs at the resource level. Ours requests contents: read and metadata: read. Nothing at write level, because nothing writes.
Tokens are short lived. The app does not store a repository access token at all. It stores an installation ID, which is not a secret. When a scan runs, the server signs a short JWT with the app's private key, exchanges it for an installation token valid for one hour, and uses that. Nothing long lived exists to leak.
Nothing reaches the browser. The client never holds a GitHub credential in any form.
The cost
It is more work. An OAuth flow is a redirect and a token exchange. A GitHub App needs:
- an app registration with declared permissions
- RS256 JWT signing with the app's private key
- an installation token exchange, cached in memory for its one hour life
- an install flow that is distinct from sign in
- a separate user authorization step, because the app still needs to know the user's GitHub login in order to filter commits by author
That last one catches people out. Installing an app does not tell you who installed it. You need to enable user authorization during installation and exchange the resulting code, or every scan returns zero commits because it is filtering by an author it does not know.
What we kept Firebase Auth for
Identity, and nothing else. Firebase Auth answers "who is this person" and issues the session. The GitHub App answers "what may this product read". Keeping those separate is what let us drop the repo scope entirely.
The sign in screen now asks for user:email and nothing more. The scary consent screen went away, and the product reads less than it did before.