Skip to content

feat: log in through OAuth 2.0 device code by default - #1402

Draft
vladfrangu wants to merge 1 commit into
masterfrom
feat/oauth-device-code-login
Draft

vladfrangu wants to merge 1 commit into
masterfrom
feat/oauth-device-code-login

Conversation

@vladfrangu

@vladfrangu vladfrangu commented Sep 8, 2026 •

Copy link
Copy Markdown
Member

apify login now defaults to OAuth 2.0 against the Console authorization server instead of the bespoke Console hand-off. No new dependencies; native fetch and node:crypto only.

Flow (--method oauth2, the new default; console and manual unchanged, --token still short-circuits)

  • Device code first: prints the verification URL and code, opens the browser, polls the token endpoint at the server-provided interval.
  • Falls back to authorization code + PKCE on a loopback server, then to the legacy Console hand-off, but only on capability failures. A denied or expired login stops with an error; Ctrl+C prints Login cancelled.
  • Discovery is RFC 8414 from APIFY_CLI_OAUTH_ISSUER_URL (default https://console-backend.apify.com). The client ID is the URL of a client metadata document, APIFY_CLI_OAUTH_CLIENT_ID (default https://apify.com/.well-known/oauth-clients/apify-cli.json, served since apify/apify-web#6599).

Sessions, per stored account

  • The access token is a regular Apify API token that expires after an hour, so resolveAuth (both the active and the --profile branch) and getLocalUserInfo go through getAccessToken(userId), which refreshes when under 60 s remain or when the refresh token has under 30 min left. One refresh attempt per process and account; a best-effort lock file plus an invalid_grant re-read handle concurrent CLIs, since the server rotates refresh tokens.
  • The profile's reserved fields are now read: authMethod: 'oauth2', expiresAt, hasRefreshToken, plus an oauth block with issuer, client ID, token endpoint and refresh-token expiry. The refresh token is a third per-user secret kind, refresh-token, so it lives in the keyring service com.apify.cli.refresh-token (inline on the profile on the file backend) and follows the token on a keyring fallback, a --profile logout and logout --all.
  • loginWithToken stays the only credential writer. It drops any OAuth session of that account; an OAuth login validates the token through it and saves the session right after.
  • apify run requests a token with at least 45 min left and warns only if it could not get one. A session that cannot be refreshed degrades to a warning and the Actor runs without API access. apify auth token notes the expiry on stderr.

Not in this PR

  • apify mcp install embeds the resolved token in the client config; with an OAuth login that token dies after an hour, so it should warn or ask for --token.
  • apify auth list does not yet show how an account logged in or when its session expires.
  • Local Actor runs longer than an hour lose API access; needs longer-lived tokens or a refresh hook from the platform.
  • No revocation endpoint exists, so logout cannot invalidate the 60-day refresh token server-side.

@github-actions github-actions Bot added this to the 149th sprint - Tooling team milestone Sep 8, 2026
@github-actions github-actions Bot added t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics. labels Sep 8, 2026
@vladfrangu
vladfrangu force-pushed the feat/oauth-device-code-login branch from c1552cc to 0bb110b Compare September 30, 2026 13:41
@l2ysho

l2ysho commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Sorry for I am really late with this. I tested it locally (rebased on master + some small changes). I was able to connect device flow with multi account (master) and login multiple accounts.

$ apify auth list
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│ Name                   User ID             Type           Last login               Storage   │
├──────────────────────────────────────────────────────────────────────────────────────────────┤
│ motivated_container2   AvSU*****f52Zh   Personal       2026-10-09 at 11:49:25   auth.json │
├──────────────────────────────────────────────────────────────────────────────────────────────┤
│ richardsolar           eCJxAGa*****Vvmjx   Personal       2026-10-09 at 12:00:35   auth.json │
├──────────────────────────────────────────────────────────────────────────────────────────────┤
│ balrog (active)        qTyaZT****bef6iQ   Organization   2026-10-09 at 12:00:55   auth.json │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

Good job with this, I do not see any problem right now, just one thing, we are missing some token invalidation step in logout for refresh token (other wise it is still valid up to 60d), but maybe you deliberately drop it with @valekjo

Do you have time to rebase this on master @vladfrangu ? If not maybe @apify/builders can take it over at this point?

again GJ 🚀

The oauth2 method is the new default for apify login: device code first, then authorization code with PKCE on a loopback server, then the legacy Console hand-off. Access tokens are refreshed transparently before they expire; the refresh token lives in the OS keyring next to the token.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants