Building a Google OAuth CLI in Rust with PKCE
TL;DR: I built a tiny CLI that opens a Google login in your browser, receives the OAuth callback, exchanges the code using PKCE, and prints basic public profile info (email, name, picture). It took me about 5 hours in Rust, mainly because of ownership, String vs &str, and lifetime wrangling—but the result is a clean, secure local flow that avoids shipping secrets in source control.
Repository: Source Code
Why I did this
I wondered how Github CLI login works under the hood, and I wanted to build a similar flow for Google. So I decided to implement it myself.
Also: I wanted to do it in Rust.
The minimal shape of the flow
High level, the app does these things:
- Load client config from environment variables
- Generate a PKCE
code_verifierandcode_challenge - Start a local HTTP server bound to
localhost:0(OS chooses the port) - Build and print the Google authorization URL
- You open the URL and log in
- Google redirects to
http://localhost:<port>/oauth/google/callback?code=... - The app exchanges the
codefor tokens using the originalcode_verifier - The app decodes the
id_tokenpayload and prints basic public profile info:- name
- picture
That's it. No browser automation, no pasting codes - just a local browser flow.
Why PKCE?
If you build a non-confidential client (CLI, desktop, mobile), you can't assume a client secret will stay secret. PKCE ensures that even if an authorization code is intercepted, the attacker can't redeem it without the original code_verifier. The server verifies that the code_challenge sent during login matches the code_verifier sent during the token exchange.
The Rust experience (short)
- It took longer than Python/JS would have. Much longer.
- The protocol itself is simple; the trouble in my case was dealing with lifetimes, moving/borrowing strings into collections, and convincing the compiler that nothing will outlive its owner.
- The result is robust and fast, but plan for extra time if you're learning Rust and OAuth at the same time.
If you're in a hurry and don't care about learning Rust, use Python or JS for a prototype.
Important setup note
Before running this, create an OAuth client in Google Cloud Console and choose "Desktop app" as the client type. This makes the client configuration suitable for local callback flows. After that, copy /.env.example to /.env and fill only these two values:
GOOGLE_OAUTH_CLIENT_IDGOOGLE_OAUTH_CLIENT_SECRET
Everything else (scope, authorization/token endpoints) can be copied from .env.example shipped in the repo.
Running it
- Create the desktop OAuth client in Google Cloud.
- Copy
.env.exampleto.envand setGOOGLE_OAUTH_CLIENT_IDandGOOGLE_OAUTH_CLIENT_SECRET. -
cargo run --release(or build and run the binary). - The CLI prints an authorization URL—open it in your browser and log in.
- After the Google consent screen, the browser will redirect to the temporary localhost server; the CLI prints your
email,name, andpicture.
Note: the server binds to localhost:0, so the OS chooses a free port. This avoids port collisions.
Security caveats (this is a learning project)
- I use PKCE, which is appropriate for public clients.
- The ID token payload is decoded and read; I do not (in this learning version) fully validate the JWT signature, issuer, audience, nonce, or expiration. For production use, you must validate these claims and verify the token signature.
- The app reads client secrets from
.env.
Closing thoughts
This project was my way to learn OAuth (the protocol) and Rust (the language) together. The final flow is clean: the CLI asks the user to log in and returns the public profile info without ever storing secrets in the repo.
If you want to try it:
- Fork or clone the repo
- Create a Desktop OAuth client in Google Cloud
- Set the two env vars and run it
Top comments (0)