Juno

Connect an app with OAuth

Let other people connect your app to their own Juno workspace. Juno is an OAuth 2.1 authorization server with discovery, dynamic client registration, and PKCE.

6 topics5 min read

OAuth is for apps other people connect to their own Juno workspace. Your app never sees a Juno password or an API key. The person signs in to Juno, chooses a workspace, chooses which permissions to grant, and can disconnect later. Juno's REST API and MCP server both accept the resulting access token.

Discovery

Juno publishes the two standard discovery documents. The authorization server document at /.well-known/oauth-authorization-server lists the endpoints, the grant types, and the permissions on offer. A protected resource document names each surface you can reach: /.well-known/oauth-protected-resource/v1/mcp for the MCP server, and /.well-known/oauth-protected-resource/v1/api for the REST API.

A client that has no token does not need to be configured with any of this. Call the endpoint without a token. Juno answers 401 and names the matching discovery document in the WWW-Authenticate header, so the client can find the authorization server on its own.

// A call with no token names its own discovery document.
$ curl -i -X POST https://api.heyjuno.co/v1/mcp/

HTTP/2 401
www-authenticate: Bearer resource_metadata="https://api.heyjuno.co/.well-known/oauth-protected-resource/v1/mcp"

Register your app

Register by posting your client metadata to /v1/oauth/register. No credential is needed to register. Juno returns a client_id beginning jnoc_. Ask for token_endpoint_auth_method 'none' and you get a public client with no secret, which is what a desktop or command-line client wants.

curl -X POST https://api.heyjuno.co/v1/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Acme Research Copilot",
    "client_uri": "https://acme.example",
    "redirect_uris": ["https://acme.example/callback"],
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "token_endpoint_auth_method": "none",
    "scope": "studies:read studies:write links:read"
  }'
New registrations expire A registration that never completes a connection expires after seven days and is cleaned up. Once someone approves your app, the registration stops expiring.

The authorisation flow

Juno supports the authorization code grant with PKCE, and only the S256 challenge method. A request with no code challenge, or one that asks for the plain method, is refused. Always send an explicit redirect_uri, even if you registered only one.

Send the resource parameter naming the surface you want: https://api.heyjuno.co/v1/mcp or https://api.heyjuno.co/v1/api. A token is bound to the resource it was issued for.

  1. 1 Send the person to /v1/oauth/authorize with your client_id, redirect_uri, the scopes you want, the resource, and an S256 code challenge.
  2. 2 Juno takes them to its own consent screen. They sign in if they are not signed in already, and verify their identity if their workspace requires two-step verification.
  3. 3 Juno returns them to your redirect_uri with an authorization code. The code is valid for five minutes and can be used once.
  4. 4 Exchange the code at /v1/oauth/token with your code verifier. You get an access token and a refresh token.
https://api.heyjuno.co/v1/oauth/authorize
  ?response_type=code
  &client_id=jnoc_9d41e6b8a3f2f6c1d9e4b7a0
  &redirect_uri=https%3A%2F%2Facme.example%2Fcallback
  &scope=studies%3Aread%20studies%3Awrite%20links%3Aread
  &resource=https%3A%2F%2Fapi.heyjuno.co%2Fv1%2Fmcp
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
  &state=9f2c1d9e4b7a

The consent screen shows your app's name, the web address it runs on, and the address Juno will return to. It says plainly that the name is supplied by the app, so the reader should check the addresses. They then choose which workspace to connect and tick the permissions to grant. They can grant fewer than you asked for, and their role in that workspace can rule some out. They can deny.

  • studies:read: Read studies, study jobs, and simulations.
  • studies:write: Create and update studies, run a simulation, and change technical collection states. Go live needs its own scope.
  • studies:golive: Set study mode to 'live'. This performs Go live.
  • links:read: Read the invite link for a study.
  • links:write: Create a participant link that carries context about the people it is for.
  • export:read: List and read participant interviews, and request and download exports.
Scopes OAuth offers six of the seven scopes. agent:run is not among them, because A2A accepts only an API key. Ask for the least you need: a person reading a long list of permissions is being asked to take your word for each one.

Tokens

Access tokens begin jnoa_ and last at most ten minutes. They are opaque, so do not try to read anything out of them. Refresh tokens begin jnor_. The connection itself lasts thirty days from the moment it was approved, and neither token outlives it.

Refresh from one place only Refresh tokens rotate. Every refresh returns a new one and retires the old one. Store the new token before you use it. Presenting a refresh token that was already spent is treated as a leak and revokes the whole connection, so never refresh from two places at once.

Disconnect

The person who approved the connection can end it in the Juno app under Settings, then Connected apps. Your app can end it too, by posting the token to /v1/oauth/revoke, which revokes the whole connection rather than the single token. Either way the next call gets a 401, so treat that as a normal outcome and ask to reconnect rather than retrying.

Connect an MCP client How the hosted Juno MCP server accepts this token. Use an API key instead For your own backend, where nobody else has to approve anything.