惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News | PayPal Newsroom
云风的 BLOG
云风的 BLOG
GbyAI
GbyAI
Engineering at Meta
Engineering at Meta
B
Blog RSS Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Register - Security
The Register - Security
L
LangChain Blog
A
About on SuperTechFans
S
Schneier on Security
博客园 - 三生石上(FineUI控件)
Stack Overflow Blog
Stack Overflow Blog
The Hacker News
The Hacker News
AWS News Blog
AWS News Blog
博客园 - 司徒正美
Scott Helme
Scott Helme
K
Kaspersky official blog
Cyberwarzone
Cyberwarzone
T
Tenable Blog
腾讯CDC
Recorded Future
Recorded Future
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
G
GRAHAM CLULEY
Security Latest
Security Latest
S
Securelist
D
Darknet – Hacking Tools, Hacker News & Cyber Security
aimingoo的专栏
aimingoo的专栏
Google DeepMind News
Google DeepMind News
V
Vulnerabilities – Threatpost
雷峰网
雷峰网
T
The Exploit Database - CXSecurity.com
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
V2EX
T
The Blog of Author Tim Ferriss
D
Docker
S
Security Affairs
F
Full Disclosure
Know Your Adversary
Know Your Adversary
N
News and Events Feed by Topic
N
News and Events Feed by Topic
T
Tor Project blog
Hugging Face - Blog
Hugging Face - Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Microsoft Security Blog
Microsoft Security Blog
Simon Willison's Weblog
Simon Willison's Weblog
Recent Announcements
Recent Announcements
博客园_首页
博客园 - 聂微东
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
S
Security @ Cisco Blogs

The JetBrains Blog

JetBrains Air lands on Windows - The JetBrains Blog The Role of Static Code Analysis in Fintech Compliance Kotlin Notebook Sunset - The JetBrains Blog Open-Sourcing the LSP Client API in IntelliJ IDEA 2026.2 - The JetBrains Blog The Dev Containers Story: Introducing EelApi for Plugin Authors - The JetBrains Blog Cursor's $60B Acquisition - Qodana Codex is now the recommended agent in JetBrains IDEs - The JetBrains Blog SSH Connections Are Moving to JetBrains Daemon in the Toolbox App 3.6 EAP - The JetBrains Blog Your AI Agent Keeps Missing The Real Bottleneck. JetBrains Rider Can Fix It Now. - The JetBrains Blog Rust Web Development 2026: The Problems Nobody Talks About Our Research on Membership Inference Attacks and Preventing Privacy Leaks - The JetBrains Blog Explicit Lazy Imports Are Coming to Python 3.15 - The JetBrains Blog Kotlin Toolchain 0.11: The Next Step for Amper - The JetBrains Blog YouTrack Helpdesk Now Includes Customer Groups - The JetBrains Blog How to Win a Hackathon: Notes From the Judging Table - The JetBrains Blog How We Measure the ROI of JetBrains IDEs - The JetBrains Blog AWS Image Builder Plugin for TeamCity - The JetBrains Blog PHP Version Migration | Jetbrains Qodana Bamboo End of Life: How to Prepare and Choose the Right CI/CD Replacement - The JetBrains Blog Structuring IntelliJ Plugins with Optional Content Modules - The JetBrains Blog YouTrack Security Update: Upgrade Required for YouTrack Server - The JetBrains Blog Qodana Is a Finalist in the 2026 CODiE Awards for Best DevOps Tool - The JetBrains Blog JetBrains Marketplace Ecosystem Security Update: Addressing Malicious Third-Party AI Plugins - The JetBrains Blog Your JetBrains IDE Expertise, Now on LinkedIn - The JetBrains Blog The JetBrains AI Coding Agent moves to general availability Step Rejection Fine-Tuning: Squeezing More Signal from Noisy Agent Trajectories - The JetBrains Blog The Anthropic Debate - The Qodana Blog dotInsights | June 2026 | The .NET Tools Blog Inside JetPride: How JetBrains Employees Built an LGBTQIA+ Community | The Life at JetBrains Blog MPS 2026.1 Release Candidate Arrives | The MPS Blog Best Python AI Frameworks in 2026 | The PyCharm Blog Contribute to the State of PHP Survey | The PhpStorm Blog The Rules of Zero, Three and Five - The Qodana Blog Modern C++ Support in CLion: What’s New | The CLion Blog Agentic AI Governance: Designing for Accountability and Control | The JetBrains AI Blog JetBrains Plugin Developer Conf 2026 – Call for Speakers | The JetBrains Platform Blog Fewer False Positives in RustRover 2026.2|The RustRover Blog Rider 2026.2 EAP 5: Code Quality Checks for Your AI Agents, and More. | The .NET Tools Blog Why Zig Isn’t 1.0 (Yet) | The JetBrains Blog Java Annotated Monthly – June 2026  | The IntelliJ IDEA Blog IntelliJ IDEA 2026.1.3 Is Out! | The IntelliJ IDEA Blog RustRover at RustWeek 2026 | The RustRover Blog WPF Hot Reload Is Here: Edit Your XAML and Watch It Update Live in Rider | The .NET Tools Blog Kotlin 2.4.0 Released | The Kotlin Blog IntelliJ IDEA 2025.3.6 Is Out! | The IntelliJ IDEA Blog Async VFS Content Writes - What Plugin Authors Need to Know | The JetBrains Platform Blog Top Agentic Frameworks for Building Applications 2026 | The PyCharm Blog Toolbox App 3.5: Better Remote Development Observability, More Reliable Enterprise Configuration, and Smoother Everyday Interactions | The Toolbox App Blog Fix Common TypeScript Issues | The Qodana Blog Mellum2 Goes Open Source: A Fast Model for AI Workflows | The JetBrains AI Blog What Does It Actually Take for an IDE to Understand Rust? Hibernate 7.4 New Features | The IntelliJ IDEA Blog How We Use AlphaEvolve to Make Complex IDE Algorithms Faster | The JetBrains AI Blog JetBrains Academy – May Digest | The JetBrains Academy Blog TeamCity 2026.1.1 Is Now Available | The TeamCity Blog The Upcoming Sunset of DataSpell | The DataSpell Blog Deprecating dotMemory Unit | The .NET Tools Blog Koog 1.0 Is Out: Stable Core, Better Interop, and Multiplatform Observability | The JetBrains AI Blog Introducing the Cloud9 JetStream Theme for JetBrains IDEs | The JetBrains Blog Build a Live Object Detection App for the Reachy Mini With TensorFlow and PyCharm | The PyCharm Blog IntelliJ IDEA 2026.2 EAP Is Open | The IntelliJ IDEA Blog How AI Agents Can Work with TeamCity | The TeamCity Blog
Stop Pasting Tokens: OAuth2 Login for JetBrains IDE Plugins | The JetBrains Platform Blog
Jakub Chrzanowski · 2026-06-01 · via The JetBrains Blog
Platform logo

Plugin and extension development for JetBrains products.

IntelliJ Platform Plugins

Stop Pasting Tokens: OAuth2 Login for JetBrains IDE Plugins

The moment a plugin needs account data, a simple API call turns into an authentication problem. The bad shortcut is familiar: ask the user to create a personal access token (PAT), make them paste it into settings, and hope it never leaks.

For a JetBrains IDE plugin, use this flow instead: the user clicks the Login button, the browser opens, the provider handles sign-in, the IDE receives a callback, and the plugin stores the token.

At a high level, the plugin will:

  1. Open the provider’s authorization page in the browser.
  2. Receive the OAuth2 callback inside the IDE.
  3. Validate the returned state.
  4. Exchange the authorization code with PKCE.
  5. Store the access token in PasswordSafe.

This post uses GitHub as the OAuth2 provider, but the same shape works elsewhere. Scopes, URLs, token responses, and refresh rules will change.

Sample code: https://github.com/JetBrains/intellij-sdk-docs/tree/main/code_samples/oauth2

The Mental Model

OAuth2 is easier to reason about as hotel key cards.
At check-in, you do not get a master key. You get a card for your room, maybe the elevator or gym. When your stay ends, the card stops working.

That is the useful bit: allowed access, but limited and temporary. An OAuth2 access token works the same way. The user signs in with the provider, and the plugin gets a token for the API access the user approved. The plugin never needs the user’s password.

That approach is better than asking people to paste a long-lived secret into settings. Users stay in the browser login flow they already trust, while the provider keeps control of scopes, expiration, and revocation.

So the goal is simple: get the plugin a limited token without making the user paste one manually. The catch is that a desktop plugin cannot protect a traditional client secret.

Why PKCE Is Part of the Story

In a web app, the server can keep a client secret on the backend. A desktop plugin cannot do that. Anything bundled into the plugin can be inspected.

That is where PKCE comes in. PKCE stands for Proof Key for Code Exchange, and it ties the returned authorization code to the login request that created it.

Before opening the browser, the plugin creates a random code_verifier and sends GitHub a derived code_challenge. Later, when GitHub redirects back with a temporary code, the plugin sends the original verifier to the token endpoint.

GitHub compares the verifier with the earlier challenge. If they do not match, no token. That means the returned code is not enough on its own, which is exactly what we want for a desktop plugin.

The Flow

Here is the full flow:

  1. The user clicks Login with GitHub.
  2. The plugin creates state, code_verifier, and code_challenge.
  3. The plugin opens GitHub’s authorization URL in the browser.
  4. GitHub redirects back to the IDE with state and a temporary code.
  5. The plugin validates state.
  6. The plugin exchanges the code and verifier for an access token.
  7. The plugin stores the token in PasswordSafe and calls the GitHub API.

Where the Flow Lives in Code

The sample code lives in code_samples/oauth2. The flow above is split across four small pieces:

  • plugin.xml registers the settings UI and the local callback handler.
  • AuthConfigurable gives the user the login and logout buttons.
  • AuthRestService handles the request that GitHub sends back to the IDE’s built-in HTTP server.
  • AuthService creates the OAuth2 request, exchanges the code, stores the token, and calls the API.

That split is the main thing to notice. OAuth2 feels messy when everything is described as one big mechanism. In code, it is much easier to follow when each class owns one part of the trip.

Register the UI and Callback

The plugin descriptor registers two things:

  • the settings page
  • the local HTTP callback handler
<extensions defaultExtensionNs="com.intellij">
  <applicationConfigurable
      instance="org.intellij.sdk.oauth2.AuthConfigurable"
      id="org.intellij.sdk.oauth2.AuthConfigurable"
      displayName="My Plugin Auth"/>

  <httpRequestHandler implementation="org.intellij.sdk.oauth2.AuthRestService"/>
</extensions>

applicationConfigurable adds the settings page. httpRequestHandler registers a handler with the IDE’s built-in HTTP server, so a request to /api/myplugin can be routed to AuthRestService. That gives GitHub a local redirect target after browser authorization.

Keep the Settings UI Boring

AuthConfigurable is the settings UI. In the sample, it extends BoundConfigurable, uses the Kotlin UI DSL, and its job is small:

  • if disconnected, show Login with GitHub
  • if connected, show the username and Logout

The panel observes AuthService.state, and the view is a small state switch:

private fun createView(state: AuthState) = panel {
  when (state) {
    is AuthState.Connected -> row("Username") {
      label(state.username ?: "Unknown")
      button("Logout") { authService.logout() }
    }

    is AuthState.Disconnected -> row {
      button("Login with GitHub") { authService.login() }
    }
  }
}

Receive the Browser Redirect

After approval, GitHub redirects back to the IDE’s built-in HTTP server. The callback is handled with the IntelliJ Platform RestService:

http://localhost:<built-in-server-port>/api/myplugin

AuthRestService reads state and code, finds the pending login request, completes it, and returns a small HTML response:

val parameters = urlDecoder.parameters()
val state = parameters["state"]?.firstOrNull()
    ?: return "No authorization state found"
val code = parameters["code"]?.firstOrNull()
    ?: return "No authorization code found"
val callback = service<AuthService>().callbacks.remove(state)
    ?: return "No active OAuth request found"

callback.complete(code)
sendResponse(
  request,
  context,
  response("text/html", Unpooled.wrappedBuffer(HTML_RESPONSE.toByteArray()))
)
return null

After that, AuthService continues the flow by exchanging the code for a token.

Run the Flow

AuthService creates the login request, waits for the callback, and exchanges the returned code:

private suspend fun requestToken(): String {
  val state = UUID.randomUUID().toString()
  val codeVerifier = UUID.randomUUID().toString().padStart(43, '0')
  val callback = CompletableDeferred<String>().also { callbacks[state] = it }

  try {
    BrowserUtil.browse(authorizationUrl(state, codeVerifier))
    return exchangeCodeForToken(callback.await(), codeVerifier)
  } finally {
    callbacks.remove(state)?.cancel()
  }
}

CompletableDeferred is the bridge between the HTTP callback and the coroutine waiting in requestToken(). requestToken() waits on callback.await(), and AuthRestService completes that same object when GitHub redirects back with the code.

The padStart(43, '0') is there because GitHub expects the PKCE verifier to meet the minimum verifier length. Some providers are less strict and may accept a UUID as-is, but GitHub needs the verifier to be at least 43 characters long.

The authorization URL carries both safety checks: state and the PKCE challenge.

private fun authorizationUrl(state: String, codeVerifier: String) = url(
  AUTHORIZATION_URL,
  "client_id" to CLIENT_ID,
  "scope" to SCOPES,
  "state" to state,
  "redirect_uri" to redirectUri,
  "code_challenge" to codeChallenge(codeVerifier),
  "code_challenge_method" to "S256",
)

The challenge is derived from the code verifier:

private fun codeChallenge(codeVerifier: String) =
  DigestUtil.sha256().digest(codeVerifier.toByteArray())
    .let { Base64.getUrlEncoder().withoutPadding().encodeToString(it) }

The actual token exchange is a POST to GitHub’s token endpoint:

private suspend fun exchangeCodeForToken(code: String, codeVerifier: String) =
  withContext(Dispatchers.IO) {
    parseAccessToken(post(tokenUrl(code, codeVerifier), null).readString())
  }

The token request sends back the temporary code and the original verifier:

private fun tokenUrl(code: String, codeVerifier: String) = url(
  ACCESS_TOKEN_URL,
  "client_id" to CLIENT_ID,
  "client_secret" to CLIENT_SECRET,
  "code" to code,
  "redirect_uri" to redirectUri,
  "code_verifier" to codeVerifier,
)

The sample includes a GitHub client secret because GitHub’s OAuth app flow expects one. For a desktop plugin, do not treat that value as secret. PKCE is the useful check here: the returned code is useless without the original verifier.

Store the Token in PasswordSafe

Once the provider returns an access token, store it in PasswordSafe. Regular persistent settings are fine for preferences, but not for access tokens.

The sample uses one credential key:

private val credentials =
  CredentialAttributes(generateServiceName("MyPluginAuth", "OAuthToken"))

On startup, the service restores an existing token if one was saved earlier:

init {
  coroutineScope.launch {
    val token = PasswordSafe.instance.getPassword(credentials) ?: return@launch
    _state.value = AuthState.Connected(fetchUserProfile(token))
  }
}

Storing and clearing go through the same helper:

private fun storeToken(token: String?) =
  PasswordSafe.instance.setPassword(credentials, token)

For a real plugin, use a stable service name. If you support multiple accounts, store one credential per provider account.

Platform sources: PasswordSafe, CredentialStore, and CredentialAttributes.

Calling the API

After login, the rest of the plugin should not care how OAuth2 worked. The sample uses the external org.kohsuke:github-api library and passes the token into GitHubBuilder to fetch the current GitHub username:

private suspend fun fetchUserProfile(token: String): String? =
  withContext(Dispatchers.IO) {
    runCatching { GitHubBuilder().withOAuthToken(token).build().myself.login }
      .onFailure { thisLogger().warn("Failed to fetch user profile", it) }
      .getOrNull()
  }

Keep that boundary in larger plugins too. API code should not know how browser login works.

Wrapping Up

OAuth2 in a plugin is mostly about putting the responsibilities in the right place.

Let the provider handle sign-in. Let the browser handle the user-facing login. Let the IDE receive the callback. Let AuthService deal with the token. And once the token is stored in PasswordSafe, the rest of your plugin can stop caring how the user authenticated.

If you are building something similar, or if you hit an edge case with a provider, bring it to the JetBrains Platform forum.
Good luck!

Subscribe to JetBrains Platform updates

Discover more