Connect an app (authorization code + PKCE)
How an app that runs on a person's machine gets read-only access to the keys that person chose to share: the /connect page, the one-time code, the grant, the exchange and revocation.
An app that a person runs, such as a desktop app or a browser extension, can read some of that person's keys without holding a penv password or a long-lived token. The person approves the connection on penv.cloud and picks the keys. The app gets a grant it can trade for short-lived, read-only credentials. Only registered apps can start this flow. You can register your own in the console.
The flow is OAuth 2.0 authorization code with PKCE. We accept the S256 challenge method only.
The flow
- The app opens
/connectin the person's browser. - The person signs in if needed, picks an organization, project and environment, ticks keys and chooses Allow or Cancel.
- We redirect to the app with a one-time code, or with an error.
- The app trades the code and its PKCE verifier for a grant (
pca_). - The app trades the grant for a
pck_credential, and receives a new grant to use next time. - The app reads the environment with the
pck_credential.
1. Open /connect
https://penv.cloud/connect?client_id=example-app
&redirect_uri=https%3A%2F%2Fexample.invalid%2Fcallback
&state=fake-state-value
&code_challenge=fake-s256-challenge
&code_challenge_method=S256
&keys=EXAMPLE_API_KEY,OTHER_TOKEN
&env_hint=acme/web/staging| Parameter | Required | Meaning |
|---|---|---|
client_id | yes | The app's registered id, app_… (register an app). |
redirect_uri | yes | One of the redirect URIs registered for the app. Compared exactly, character for character. |
state | yes | A random value the app made. We return it unchanged. 1 to 1,024 printable characters. |
code_challenge | yes | The base64url SHA-256 of the app's code_verifier. |
code_challenge_method | yes | S256. Anything else is refused. |
keys | yes | The key names the app wants, comma separated. The person can untick any. |
env_hint | no | org/project/environment. Preselects that environment when the person can use it. Otherwise ignored. |
A parameter given twice is an error. The app generates a fresh state and code_verifier for every attempt, and keeps the verifier to itself.
2. The person decides
The page shows the app's name and website, lists each requested key with its status in the chosen environment, and says the access is read only. A key the environment does not have cannot be ticked. A write-only environment, and an environment that requires approval for changes, cannot be chosen. The person also needs permission to reveal values in the environment. See Connect an app for the person's side.
3. The redirect
| Choice | Redirect |
|---|---|
| Allow | redirect_uri?code=<code>&state=<state> |
| Cancel | redirect_uri?error=access_denied&state=<state> |
A malformed state, challenge, method or keys | redirect_uri?error=invalid_request, with state when it was valid |
The code works once and lasts 60 seconds. If client_id or redirect_uri is wrong we show the error on penv.cloud and never redirect, so a bad link cannot send a person to an address we did not register.
4. Trade the code for a grant
POST /api/v1/auth/app/token takes no credential.
{
"client_id": "example-app",
"code": "fake-code",
"code_verifier": "fake-code-verifier",
"redirect_uri": "https://example.invalid/callback"
}The answer is 201:
{
"grant": "pca_fake",
"connection": {
"id": "00000000-0000-0000-0000-000000000000",
"org": "acme",
"project": "web",
"environment": "staging",
"keys": ["EXAMPLE_API_KEY"]
}
}We burn the code the moment we receive it. A wrong verifier, a wrong redirect_uri or a second try therefore answers 400 and the app starts over at step 1. Store the grant like a password: it is the app's only long-lived proof.
5. Trade the grant for a credential
POST /api/v1/auth/app takes no credential.
{ "grant": "pca_fake", "ttlSeconds": 300 }The answer is 201 with credential (pck_), expiresAt and a new grant. Save the new grant and discard the old one. Each grant works once. Run one exchange at a time per connection, or two requests will present the same grant.
If a grant that was already replaced is presented again, we treat it as a copy: we answer 400 invalid_grant, revoke the whole connection with every credential it minted, and write an audit entry. The person sees the app as disconnected and the app has to ask again.
6. Read the environment
GET /api/v1/envs/{org}/{project}/{environment}
Authorization: Bearer pck_fakeThe answer holds only the keys the person ticked. Other keys are left out entirely, not masked and not counted. The credential reads; it cannot write, list other environments or reach a write-only environment. See Authentication for the rest of the credential's rules.
Lifetimes
| Item | Lifetime |
|---|---|
| Connect code | 60 seconds, one use |
pck_ credential | The shortest of ttlSeconds and 15 minutes. 15 minutes when you leave ttlSeconds out |
| Grant | Replaced at every exchange. A connection unused for 90 days expires |
ttlSeconds follows the machine exchange rules: a whole number, at least 60. A longer ask is capped, not refused; a smaller one answers 400 invalid_request with a detail.
Disconnect
| Who | How | Answer |
|---|---|---|
| The app | POST /api/v1/auth/revoke with Authorization: Bearer pca_… (the current grant) | 204 |
| The person | DELETE /api/v1/app-connections/{id} with their pcu_ credential, or the console | 204, or 404 not_found |
An app that only wants to drop the pck_ it holds sends that pck_ to /auth/revoke instead: 204, and the connection stays.
Either way the grant and every live pck_ from it stop working at once. A replaced grant sent to /auth/revoke also ends the connection, but the audit entry records it as grant reuse, as at the exchange. GET /api/v1/app-connections lists the person's live connections. The person's calls need a pcu_ credential; an app or machine credential answers 403.
The connection also ends at the next exchange, and its pck_ stops working, when the workspace starts requiring single sign-on and the person did not approve the connection through its identity provider, or, once the grace period is over, two-factor authentication the person has not set up.
Errors
Bodies are { "error": "<code>", "detail": "<text>" }; detail is optional.
| Status | Code | Cause |
|---|---|---|
| 401 | invalid_client | No registered app has that client_id, or the app was deleted (token endpoint). |
| 403 | app_unavailable | We suspended the app. Returned by the token and exchange endpoints. /connect shows it on the page. |
| 403 | app_unverified | The app is still in testing mode and the person is not a member of the organization that owns it. Returned by the token and exchange endpoints. /connect shows it on the page. |
| 400 | invalid_redirect_uri | The redirect_uri is not one registered for the app. On /connect we show this on the page and do not redirect. |
| 400 | invalid_request | A parameter is missing, repeated or malformed, or ttlSeconds is not allowed. |
| 400 | invalid_grant | The code or grant is wrong, expired, already used, replaced or revoked, or the verifier does not match. The exchange also answers this for a deleted app. |
| redirect | access_denied | The person chose Cancel. Arrives in the redirect only. |
shown on /connect | environment_write_only | The environment hides values from apps. |
shown on /connect | production_locked | Changes to the environment need a second person's approval, so apps cannot be connected to it. |
shown on /connect | no_access | The person cannot reveal values in the environment. |
| 429 | rate_limited | Too many requests. retry-after gives the wait. |
The full list is in Errors.
Security notes
S256is the only challenge method. We refuseplain.redirect_urimust match a registered address exactly. There are no wildcards and no prefix matches.- Codes are single use and last 60 seconds. We bind each to the app, the redirect address and the challenge.
- Grants rotate on every use. Presenting a replaced grant revokes the connection.
- A credential reads only the keys the person ticked, in one environment, and cannot write.
- Values never appear in the redirect, the audit log or an error body.
- These endpoints send no CORS headers. A browser extension calls them with host permissions for
penv.cloud; a web page on another origin cannot.
Authentication
Exchanging a host proof for a short-lived pck_ credential: OIDC, delivery, AWS and keypair exchanges, lifetimes, permissions, PENV_TOKEN and revocation.
Register an app
How to register an app that connects to Penv Cloud: the console page, fields, client_id, redirect URI rules, testing mode, domain proof, verification, and what happens on suspension or deletion.