# Sign in with Upvera

Upvera is an OpenID Connect provider that proves the person signing in to your product is a real human. Your site gets proof, not identity: every site knows the person by an ID made just for it.

The provider is live at `https://id.upvera.co`. Client registration opens in the Upvera app with its next release, so you can build against discovery today and register your client when the screen ships.

## What you get

- Sign-in closed to bot farms. Every sign-in carries the person's proof-of-personhood tier: browser key, device key or device attestation.
- Verified people who sign in to your product become Traction proof for your startup on Upvera. The count is observed at our provider.
- No email by default. If you ask for it, it comes only when the person allows it.

## Four steps

1. Verify your domain on Upvera with a DNS TXT record or a `.well-known` file.
2. In your startup panel, open **Sign in with Upvera › Add client**. Redirect URIs must sit under your verified domain.
3. Give your OpenID Connect library the discovery URL. Authorization code flow, PKCE S256 and nonce are required.
4. Place the button, verify the ID token against the JWKS and check `iss`.

## Button

The proof button says what it gives. Use the dark button on light backgrounds and the light button on dark ones. Don't change its text or shape.

![Sign in with Upvera, dark button](/upvera-login/upvera-login-dark-en.svg) ![Sign in with Upvera, light button](/upvera-login/upvera-login-light-en.svg)

Download the SVG and serve it from your own site: [dark](/upvera-login/upvera-login-dark-en.svg) · [light](/upvera-login/upvera-login-light-en.svg) · Turkish: [dark](/upvera-login/upvera-login-dark-tr.svg) · [light](/upvera-login/upvera-login-light-tr.svg).

```html
<a href="/auth/upvera">
  <img src="/assets/upvera-login-dark-en.svg" alt="Sign in with Upvera" width="360" height="56">
</a>
```

## Endpoints

| | |
|---|---|
| Issuer | `https://id.upvera.co` |
| Discovery | `https://id.upvera.co/.well-known/openid-configuration` |
| authorize | `https://id.upvera.co/authorize` |
| token | `https://id.upvera.co/token` |
| userinfo | `https://id.upvera.co/userinfo` |
| jwks | `https://id.upvera.co/.well-known/jwks.json` |

```bash
curl -s https://id.upvera.co/.well-known/openid-configuration \
  | jq '{issuer, scopes_supported, subject_types_supported}'
```

```json
{
  "issuer": "https://id.upvera.co",
  "scopes_supported": ["openid", "profile", "email", "upvera:founder"],
  "subject_types_supported": ["pairwise"]
}
```

## Rules

- Authorization code flow only (`response_type=code`). There is no implicit flow.
- PKCE S256 is required. `plain` isn't accepted.
- `nonce` and `state` are required. Check `iss` in the response (RFC 9207).
- `redirect_uri` must match the registered one exactly, with no query and no fragment.
- ID tokens are ES256 with a `kid`. Keys rotate: cache the JWKS, and refresh it when you see an unknown `kid`.
- A code lives 60 seconds and works once. A second use also revokes the tokens issued from it.
- If you use an identity broker such as Supabase Auth, Clerk or Auth0, connect your own domain for the redirect URI.

Client authentication can be `client_secret_basic`, `client_secret_post`, `private_key_jwt` (ES256 or RS256) or `none` for public clients. Client IDs start with `upv_` and secrets with `upvs_`.

## Claims

| Claim | Meaning |
|---|---|
| `sub` | A stable ID just for your site, 22 characters. Web and mobile clients of the same startup on the same domain see the same value. Another startup sees the same person under a different value. |
| `upvera:human` | The proof-of-personhood tier and the month it was reached, for example `{ "tier": "device_key", "since": "2026-05" }`. Weigh it by your own rule: `unsigned` < `web_key` < `device_key` < `device_attested`. |
| `name` · `picture` | With the `profile` scope, if the person allows. |
| `email` · `email_verified` | With the `email` scope, if the person turns it on. Off by default. |
| `upvera:founder` | With the `upvera:founder` scope: `true` if the person is a verified founder and allows it. |

## Example in Node

Using [openid-client](https://github.com/panva/openid-client):

```js
import * as client from 'openid-client';

// 1) discovery + client authentication (registered method: client_secret_basic)
const config = await client.discovery(
  new URL('https://id.upvera.co'),
  process.env.UPVERA_CLIENT_ID,      // upv_…
  undefined,
  client.ClientSecretBasic(process.env.UPVERA_CLIENT_SECRET), // upvs_…
);

// 2) sign-in: PKCE S256 + nonce + state (keep them in the session)
const code_verifier = client.randomPKCECodeVerifier();
const nonce = client.randomNonce();
const state = client.randomState();
const url = client.buildAuthorizationUrl(config, {
  redirect_uri: 'https://example.com/auth/upvera/callback',
  scope: 'openid profile',
  code_challenge: await client.calculatePKCECodeChallenge(code_verifier),
  code_challenge_method: 'S256',
  nonce,
  state,
});

// 3) callback: iss (RFC 9207), state and nonce are checked, the ID token is verified against the JWKS
const tokens = await client.authorizationCodeGrant(config, new URL(req.url, 'https://example.com'), {
  pkceCodeVerifier: code_verifier,
  expectedNonce: nonce,
  expectedState: state,
});
const claims = tokens.claims();
// claims.sub               → an ID just for this site
// claims['upvera:human']   → { tier: 'web_key' | 'device_key' | 'device_attested' | 'unsigned', since: '2026-05' }
```

## Limits

`/authorize` accepts 30 requests per minute per IP and `/token` 60. Codes live 60 seconds, ID tokens 10 minutes.

## Privacy

Upvera keeps which products a person signs in to only as a day bucket and a hash, for 400 days. The person can disconnect at any time. Public counts under 5 people aren't shown.

Questions and security reports: destek@upvera.co

---
Source: https://upvera.dev/sign-in/ · Markdown: https://upvera.dev/sign-in.md
