Integrating "Login with mycorra"
This guide is for third-party app developers who want their users to sign in with mycorra and run LLM inference against the API keys those users have stored in their mycorra account. Your app never sees the raw provider keys — mycorra holds them and proxies inference on the user's behalf.
mycorra implements the standard OAuth 2.0 authorization-code grant (RFC 6749, §4.1). If you've integrated "Sign in with GitHub/Google", this is the same shape.
- Base URL below is written as
https://mycorra.example— substitute your
deployment (locally, http://localhost:8080).
- All timestamps are Unix seconds. Authorization codes live 5 minutes;
access tokens live 1 hour.
At a glance
1. Once: register your app → get client_id + client_secret (§1). 2. Per user: send them to /oauth/authorize → they consent → you get a code (§2). 3. Per user: exchange code at /oauth/token → access_token (§3). 4. Per request: GET /api/providers to see what they configured (§4), then POST /api/inference/{provider} with the provider's native body (§5).
The access token is the only credential you need for inference. Keep it server-side; refresh by re-running steps 2–3 when it expires (1 h) or a call returns 401.
1. Register your app (one time)
While logged in to mycorra, register an OAuth client. The redirect_uris field is newline-separated; every value must be an exact URL your app will send as redirect_uri later (no wildcards, no path-prefix matching).
curl -sX POST https://mycorra.example/clients \
-b cookies.txt \ # a mycorra session cookie
--data-urlencode 'name=My Cool App' \
--data-urlencode $'redirect_uris=https://mycoolapp.com/auth/callback'
Response — the secret is shown exactly once, store it securely:
{
"client_id": "b1c2...",
"client_secret": "9f8e...",
"redirect_uris": ["https://mycoolapp.com/auth/callback"]
}
2. Send the user to authorize
Redirect the user's browser to /oauth/authorize:
GET https://mycorra.example/oauth/authorize
?response_type=code
&client_id=b1c2...
&redirect_uri=https://mycoolapp.com/auth/callback
&state=<opaque-random-anti-CSRF-value>
| Param | Required | Notes |
|---|---|---|
response_type | yes | must be code |
client_id | yes | from step 1 |
redirect_uri | yes | must exactly match a registered URI |
state | strongly recommended | opaque value you generate; verify it on return to prevent CSRF |
- If the user isn't signed in to mycorra, they're bounced to login and back.
- They see a consent screen naming your app. On Allow, mycorra redirects to:
https://mycoolapp.com/auth/callback?code=<authorization_code>&state=<the-same-state>
Verify state matches what you sent before continuing.
3. Exchange the code for an access token
Server-to-server call (never from the browser — it uses your client secret). Client credentials may go in the body or via HTTP Basic auth.
curl -sX POST https://mycorra.example/oauth/token \
-d grant_type=authorization_code \
-d code=<authorization_code> \
-d redirect_uri=https://mycoolapp.com/auth/callback \
-d client_id=b1c2... \
-d client_secret=9f8e...
Response:
{
"access_token": "eyJ...hmac",
"token_type": "Bearer",
"expires_in": 3600
}
The code is single-use — a second exchange returns 400 {"error":"invalid_grant"}. Other error codes follow OAuth2: invalid_client, unsupported_grant_type.
4. Discover which providers the user configured
Before offering a provider, ask which ones the signed-in user actually has a key for — so you don't present options that will fail:
curl -s https://mycorra.example/api/providers \
-H "Authorization: Bearer eyJ...hmac"
{ "providers": [ { "id": "openai", "name": "OpenAI" }, { "id": "mistral", "name": "Mistral" } ] }
Only providers with a stored key are returned. An empty list means the user should add a key at /dashboard first.
5. Call inference with the access token
POST https://mycorra.example/api/inference/{provider}
Authorization: Bearer <access_token>
Content-Type: application/json
<the provider's own native request body>
{provider} is one of openai, anthropic, openrouter, mistral. mycorra does not translate schemas — you send the provider's native request body and get its native response back. mycorra only injects the user's stored key, forwards the call, meters token usage, and relays the result.
Request body per provider
OpenAI-compatible providers (openai, openrouter, mistral) — the /chat/completions schema:
{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "Say hello in French."}]
}
Anthropic — the /messages schema (note the required max_tokens):
{
"model": "claude-3-5-haiku-latest",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Say hello in French."}]
}
You may set any field the upstream provider supports (temperature, tools, etc.) — it is passed through untouched.
Example
curl -sX POST https://mycorra.example/api/inference/openai \
-H "Authorization: Bearer eyJ...hmac" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hello"}]}'
Response
On 200 the body is the provider's response verbatim. For OpenAI-compatible providers that's:
{
"id": "chatcmpl-...",
"choices": [{"message": {"role": "assistant", "content": "Bonjour !"}}],
"usage": {"prompt_tokens": 9, "completion_tokens": 3, "total_tokens": 12}
}
Anthropic returns {"content": [{"type": "text", "text": "Bonjour !"}], "usage": {"input_tokens": 9, "output_tokens": 3}}. Parse whichever shape matches the provider you called.
Errors
| Status | Meaning | What to do |
|---|---|---|
200 | upstream response, relayed as-is | — |
401 | missing / invalid / expired access token, or the user revoked your app | re-run the authorization flow (steps 2–3) |
404 | unknown {provider} | use a supported provider id |
412 | the user has no key stored for this provider | prompt them to add one, or offer a provider from /api/providers |
502 | mycorra couldn't reach the upstream provider | retry with backoff |
| other | any non-2xx from the upstream (e.g. 402 insufficient credits, 429 rate limit) is relayed as-is | surface the provider's error body to the user |
Because non-2xx upstream responses are passed through, always be ready to read an error body shaped like the provider's own error (not mycorra's).
Notes
- Streaming is not supported. Do not send
"stream": true— mycorra buffers
the full response to meter usage. Requests complete in a single JSON body.
- Usage is metered automatically. Every successful call is logged (tokens in,
tokens out, estimated cost) and visible to the user on their mycorra usage page. Your app doesn't need to report anything.
- Bodies are limited to 1 MiB in, 8 MiB out.
Security notes
- Redirect URIs are matched exactly — this closes the open-redirect hole.
Register every callback you use.
- Access tokens are stateless HMAC-signed tokens scoped to
kind=access;
a mycorra session cookie can't be replayed as an API token and vice versa.
- Never expose your
client_secretin browser/mobile code. If you can't
keep a secret (SPA/native), treat this as a confidential-client-only integration and proxy the token exchange through your backend.
- Keys never leave mycorra. Your app authenticates the *user*; mycorra
performs inference. You cannot read a user's provider key through any endpoint.
- Users can revoke your app. From their mycorra "Connected apps" page a user
can end your app's session; outstanding access tokens then return 401. Treat a sudden 401 as "re-run the authorization flow", not a hard error.
Endpoint reference
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /clients | mycorra session | register an OAuth client |
| GET | /oauth/authorize | user session | consent screen, issues a code |
| POST | /oauth/token | client secret | exchange code → access token |
| GET | /api/providers | Bearer access | list the user's configured providers |
| POST | /api/inference/{provider} | Bearer access | proxy inference with user's key |
A minimal, self-contained example of the whole flow lives in the end-to-end test: internal/web/web_test.go → TestOAuthLoginWithMycorraEndToEnd.