cli/oauth: OAuth device flow with a localhost fallback for Go CLIs
A library for performing OAuth Device flow and Web application flow in Go client apps.
At a glance
- What is it?
- The cli/oauth library is a Go package for client applications that need OAuth authorization without a browser redirect, typically against GitHub. It implements the device authorization grant and falls back to a local HTTP server that receives the browser redirect, and it has almost no dependencies.
- Who is it for?
- cli/oauth fits a Go command line tool that needs to authorize against GitHub and wants the device flow with an automatic fallback, and that wants to avoid pulling an OAuth framework with its own opinions about HTTP clients and token storage.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem it solves, stated as a browser redirect
cli/oauth is a library for Go client applications that need to perform OAuth authorization against a server, typically GitHub.com. The problem it addresses is described precisely in the README.
Traditionally, OAuth for web applications involves redirecting to a URI after the user authorizes an app. While web apps, and some native client apps, can receive a browser redirect, client apps such as CLI applications do not have such an option. A command line program has no window to redirect into, and the browser it does have may be on a different machine entirely.
The library's answer is the OAuth Device Authorization Grant, which GitHub.com now supports. With device flow, the user is presented with a one-time code that they enter in a web browser while authorizing the app on the server. The README is precise about when this fits: device flow is suitable for cases where the web browser may be running on a separate device than the client app itself, for example a CLI application could run within a headless, containerized instance, while the user completes authorization using a browser on their phone.
That containerised CLI case is the one worth dwelling on, because it is where the design earns its keep. A CI job or a server-side tool that needs a GitHub token cannot assume a browser on the same host, and the device flow does not require one. The alternative would be a personal access token pasted into an environment variable, which is exactly the pattern the device flow exists to replace.
The project is written in Go, licensed MIT, the default branch is main, and the most recent release is v1.3.0 from 2026-10-20, with v1.2.2 from 2026-01-28 and v1.2.1 from 2025-12-19 before it. The last push to the repository was on 2026-10-20.
Two flows, and the fallback that makes them work anywhere
The second part of the design is the part that makes the library usable on any GitHub host, including instances without OAuth device flow support. The library bundles an implementation of the OAuth web application flow in which the client app starts a local server at `http://127.0.0.1:<port>/` that acts as a receiver for the browser redirect.
The order matters. First, device flow is attempted, and the localhost server is used as fallback. With the localhost server, the user's web browser must be running on the same machine as the client application itself. So the library attempts the flow that works everywhere, and falls back to the one that only works when a browser is local.
The consequence for a user is that a headless server gets the device flow, and a developer on a laptop with a browser gets the fallback without having to think about it, and the README's claim of transparently enabling OAuth authorization on any GitHub host, for example GHES instances without device flow support, follows from that ordering.
The `Flow` type is the entry point, and it is configured with a host, a client ID, a list of scopes, and the refresh token switch.
flow := &oauth.Flow{
Host: host,
ClientID: clientID,
Scopes: []string{"repo"},
RequestRefreshToken: true,
}
requestedAt := time.Now().UTC()
token, err := flow.DetectFlow()
if err != nil {
return err
}The method that does the work is called `DetectFlow`, and the name is a good description of the behaviour. The library detects which flow the host supports and picks it. The `Scopes` field takes the OAuth scope strings directly, with `repo` here being the familiar GitHub repository scope.
Package structure, and how to take over the user experience
The repository layout is small and each file has an obvious job. There is oauth.go at the root, which is where the `Flow` type lives, along with oauth_device.go for the device flow, oauth_webapp.go for the localhost flow, and refresh.go for token refresh. The device/ and webapp/ directories hold the two sub-packages, api/ holds the request and response types, and examples_test.go sits at the root next to the per-package example files in device/examples_test.go and webapp/examples_test.go.
Those three example files are the documentation of record, and the README's Usage section is a three-item list pointing at them: OAuth device flow with fallback at the root, manual OAuth device flow in the device package, and manual OAuth web application flow in the webapp package. The root example shows the automatic detection behaviour, and the two package examples show the manual versions.
The guidance for anyone who needs a different experience is a single sentence. Applications that need more control over the user experience around authentication should directly interface with `github.com/cli/oauth/device` and `github.com/cli/oauth/webapp` packages. That is the extension point: the high-level `DetectFlow` handles the common case, and the sub-packages let you write the interaction yourself, whether that means printing the code in a different format, opening the browser yourself, or capturing the callback on a different port.
The dependency list explains how little this costs. The go.mod file declares the module as github.com/cli/oauth, requires Go 1.21, and has exactly one direct dependency, github.com/cli/browser at v1.3.0, with golang.org/x/sys as the only indirect one. For a library that performs OAuth, that is an unusually small surface, and it is a deliberate signal: this is a focused OAuth component from the cli organisation, not a general OAuth framework.
Refresh tokens, and the expiry arithmetic you have to do yourself
The refresh token support is the part of the documentation with the most detail, and the detail is instructive because it describes work the library deliberately leaves to the caller.
The switch is `RequestRefreshToken`, set to true to request an expiring access token and a refresh token. The README notes that servers which do not support expiring tokens may ignore the request and return a token without expiration metadata or a refresh token, so a caller cannot assume the refresh token field is populated.
The expiry fields are relative durations, and the README says plainly: calculate and persist an absolute expiration time when a token is issued.
expiresAt := time.Time{}
if token.ExpiresIn > 0 {
expiresAt = requestedAt.Add(token.ExpiresIn)
}
if err := saveToken(token, expiresAt); err != nil {
return err
}The `time.Time{}` zero value is used as the sentinel for a token with no expiry, which pairs with the fact that the server may not send expiry metadata at all. `requestedAt` is captured before the flow runs, so the clock starts when the request was made rather than when the response arrived, which is the more accurate base for a token that was already partly spent in the round trip.
The refresh path follows the same discipline. After loading the token and its persisted expiration time, you exchange an expired token's refresh token and persist the returned pair.
if token.RefreshToken != "" && !expiresAt.IsZero() && !time.Now().Before(expiresAt) {
requestedAt := time.Now().UTC()
refreshedToken, err := oauth.Refresh(oauth.RefreshOptions{
Host: host,
ClientID: clientID,
ClientSecret: clientSecret,
RefreshToken: token.RefreshToken,
HTTPClient: http.DefaultClient,
})The `RefreshOptions` struct takes a client secret and an `HTTPClient`, and the default value used in the example is `http.DefaultClient`, so the library does not impose a transport.
The last paragraph of the refresh section is the one that will bite you if you miss it. GitHub rotates refresh tokens on every refresh, so you must always persist the refresh token returned by `Refresh`. Rotation is optional in OAuth, and when a server omits a new refresh token, `Refresh` returns the one that was passed in. So the safe pattern is to write the returned refresh token unconditionally rather than only when it differs, because a server that does rotate will invalidate the old one.
What the README does not tell you
A short read of what is missing is worth doing before you build on this.
The library does not describe token storage. Every example calls a `saveToken` function that you have to write, which is the right division of labour for a library, but it means the storage decision, the encryption decision and the file permissions decision are all yours.
It does not describe how the device flow surfaces the one-time code to the user. The README says the user is presented with a one-time code and enters it in a browser, and the root example file is where the actual behaviour lives, so the console output format is a thing to look at in the example rather than in the prose.
It does not document error handling in any detail. The `DetectFlow` call returns an error, and the examples propagate it, but there is no catalogue of failure modes, no statement about what happens when the user declines at the browser, and no description of the polling interval or timeout the device flow uses. If your tool needs to tell the user what went wrong, you will be reading the source.
On the host support question the README is unusually candid. The packages would in theory enable authorization on any OAuth-enabled host, but in practice this was only tested for authorizing with GitHub. That is the honest statement, and it is the reason the library is tied to the `cli` organisation and to the GitHub-shaped fields like `Scopes: []string{"repo"}` rather than being a general OAuth client. The repository is also small enough that a reader can hold it in their head: seven Go files at the top level, two sub-packages, an api/ directory of types, and a go.mod with one dependency.
Where cli/oauth is the wrong choice
Three cases point elsewhere. The first is a tool that needs to authorize against something other than GitHub. The README says the packages were only tested for authorizing with GitHub, and while the code is written against generic OAuth concepts, the details that differ between providers, the device flow endpoints, the token endpoint, the scope names and the error shapes, are the details you will hit first. For a non-GitHub provider, golang.org/x/oauth2 is the better-known starting point, and it also has a device flow package.
The second is a tool that cannot bind a local TCP port. The fallback depends on starting a server at `http://127.0.0.1:<port>/`, and while the device flow is tried first, a program that runs in an environment where binding a port is blocked or where the browser is on a different machine will not get the fallback. A pure device flow client that never starts a server is the better fit there, and the device/ sub-package is where to look for it.
The third is a project that wants a framework rather than a component. There is no token cache, no keyring integration, no configuration file handling, no HTTP client middleware for adding the bearer token, and no consent screen. This library gets a token and hands it to you.
The alternative worth weighing is golang.org/x/oauth2, and the difference in approach is instructive. A general framework such as golang.org/x/oauth2 takes a different approach: a config object, a token source, a client that attaches the token automatically, and its own device flow support. cli/oauth is a small, dependency-light component with one dependency and a Go 1.21 floor, and it does two things that general framework does not do in the same way. It detects the flow automatically, so a user on a GHES instance without device flow still gets authorized, and it falls back to a localhost receiver server that the caller can then reach through the webapp/ sub-package. If your tool only ever talks to GitHub, that automatic detection is worth the narrower scope. If you need several providers, or you want the token already attached to your HTTP client, the framework is less work.
Editorial conclusion
cli/oauth fits a Go command line tool that needs to authorize against GitHub and wants the device flow with an automatic fallback, and that wants to avoid pulling an OAuth framework with its own opinions about HTTP clients and token storage. It does not fit an application that needs OAuth against a host other than GitHub, because the README says the packages were only tested for authorizing with GitHub, and it does not fit a tool that cannot bind a local port, since the fallback depends on it. Before you adopt it, read the token persistence example carefully, because the library hands you a relative expiry and the absolute time is yours to calculate and store.
Frequently asked questions
What is the cli/oauth Go library?
It is a library for Go client applications that need to perform OAuth authorization against a server, typically GitHub.com. It implements the OAuth Device Authorization Grant and bundles an OAuth web application flow that starts a local server on 127.0.0.1 to receive the browser redirect, using the localhost server as a fallback.
Why does cli/oauth implement the OAuth device flow?
Because client apps such as CLI applications cannot receive a browser redirect, and device flow is suitable where the web browser may be running on a separate device than the client app, for example a CLI running in a headless containerized instance while the user authorizes with a browser on their phone.
How does cli/oauth detect which OAuth flow to use?
You build an oauth.Flow with the host, client ID, scopes and the RequestRefreshToken flag, then call DetectFlow. The library attempts device flow first and falls back to the localhost web application flow, which is why it works on any GitHub host including GHES instances without device flow support.
How do I handle refresh tokens with cli/oauth?
Set RequestRefreshToken to true on the flow, then use oauth.Refresh with RefreshOptions when the stored token has expired. The expiry fields are relative durations, so you must calculate and persist an absolute expiration time, and because GitHub rotates refresh tokens on every refresh you should always persist the refresh token returned by Refresh.
Which OAuth hosts does cli/oauth work with?
The README says the packages would in theory enable authorization on any OAuth-enabled host, but in practice this was only tested for authorizing with GitHub. The library has one direct dependency, github.com/cli/browser at v1.3.0, and requires Go 1.21.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/cli-oauth)Was this useful?