OAuth Flow
The mock IDP implements the OAuth 2.0 authorization code flow, with optional PKCE (S256) and OpenID Connect ID tokens. Only the authorization_code grant is supported — there are no implicit, password, or client-credentials grants, and no refresh tokens.
All endpoints live under https://minutemail.co/idp:
| Endpoint | Method | Purpose |
|---|---|---|
/idp/oauth/authorize | GET | Show the consent screen and issue an authorization code |
/idp/oauth/token | POST | Exchange an authorization code for an access token |
/idp/oauth/userinfo | GET | Fetch the mock identity’s profile with an access token |
/idp/jwks | GET | Public RSA keys for verifying ID token signatures |
/idp/.well-known/openid-configuration | GET | OIDC discovery document |
1. Authorization request
Section titled “1. Authorization request”Redirect the user’s browser to the authorize endpoint:
GET https://minutemail.co/idp/oauth/authorize?client_id=mc_EXAMPLECLIENTID&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&response_type=code&scope=openid%20email%20profile&state=xyz123| Parameter | Required | Description |
|---|---|---|
client_id | Yes | Client ID from the dashboard |
redirect_uri | Yes | Must exactly match one of the client’s registered redirect URIs |
response_type | — | Conventionally code; the flow is always authorization code |
scope | No | Space-separated scopes. Supported: openid, email, profile. Defaults to all three if omitted |
state | Recommended | Opaque value echoed back on redirect; use it for CSRF protection |
code_challenge | Recommended | PKCE challenge (S256), for public clients |
code_challenge_method | With code_challenge | Must be S256 |
Consent
Section titled “Consent”MinuteMail renders a consent screen showing the client’s provider logo, name, the requested scopes, and the mock identity that will be used — the client’s first active identity (oldest created). Inactive identities are skipped. If the selected identity has emailVerified: false, the consent screen also shows a ”⚠ email not verified” warning next to it.
- Authorize — the browser is redirected (302) to your
redirect_uriwith the code (Apple clients instead deliver the response as an auto-submitting form POST — see Apple):
HTTP/1.1 302 FoundLocation: http://localhost:3000/callback?code=ac_EXAMPLECODE&state=xyz123- Deny — the redirect carries an error instead, with your
statepreserved:
Location: http://localhost:3000/callback?error=access_denied&error_description=The%20user%20denied%20the%20authorization%20request.&state=xyz123Authorization details:
- Codes are valid for 10 minutes and are single-use — a second exchange of the same code fails with
invalid_grant - If the client has no active identities, the consent screen shows an error instead of a code
2. Token exchange
Section titled “2. Token exchange”Exchange the code server-to-server with a form-encoded POST:
POST https://minutemail.co/idp/oauth/tokenContent-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=ac_EXAMPLECODE&client_id=mc_EXAMPLECLIENTID&client_secret=cs_EXAMPLESECRET&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&code_verifier=EXAMPLEVERIFIER| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be authorization_code |
code | Yes | The authorization code from the redirect |
client_id | Yes | The client’s ID |
client_secret | Yes | The client’s secret — verified on every exchange, including PKCE clients |
redirect_uri | Yes | The same redirect_uri used in the authorize request |
code_verifier | If PKCE was used | The plaintext verifier matching the code_challenge |
Successful response:
{ "access_token": "at_EXAMPLEACCESSTOKEN", "token_type": "Bearer", "expires_in": 3600, "scope": "openid email profile", "id_token": "eyJhbGciOiJSUzI1NiIs..."}| Field | Description |
|---|---|
access_token | Opaque Bearer token, valid for 1 hour |
token_type | Bearer — except GitHub and Facebook clients, which return "bearer" (lowercase) like the real providers |
expires_in | Lifetime in seconds (3600) — omitted for GitHub clients, whose real-world app tokens don’t expire |
scope | The scopes granted |
id_token | Present only when the scope includes openid — and never for GitHub or Facebook clients, which are plain OAuth2 providers |
These per-provider differences are deliberate and mirror the real providers’ wire behavior — see the Provider Behavior matrix.
The ID token is a signed JWT (RS256) carrying the identity’s claims — verify its signature against the keys at https://minutemail.co/idp/jwks. The claim set depends on the provider type (see Provider Behavior for the full matrix):
| Claim | Value |
|---|---|
iss | The issuer URL |
sub | The mock identity’s ID |
aud | Your client_id |
email | The linked mailbox address |
email_verified | The identity’s emailVerified field — true unless you set it to false. A boolean for all providers except Apple, which carries the string "true"/"false" like real Apple |
name, preferred_username, picture | The identity’s profile fields. Google clients use Google’s fixed claim set (given_name, family_name, locale, azp, …) and carry no name for Apple clients — the name arrives once in the form POST body instead |
nonce | Echoed from the authorization request |
| custom claims | The identity’s claims map, merged into the token (e.g. roles) — custom-provider clients only |
Standard claims cannot be overridden — custom claim names that collide with the standard set are rejected when the identity is created or updated (see Clients & Identities).
Token errors
Section titled “Token errors”Errors use the standard OAuth error format with Cache-Control: no-store:
{ "error": "invalid_grant", "error_description": "Authorization code has expired."}| HTTP | error | Cause |
|---|---|---|
| 400 | invalid_request | Missing code or client_id, or malformed form data |
| 400 | unsupported_grant_type | grant_type is not authorization_code |
| 400 | invalid_grant | Code not found, expired, already used, or redirect_uri mismatch |
| 401 | invalid_client | Bad client_id/client_secret, or PKCE verification failed |
| 500 | server_error | Unexpected server error |
3. Userinfo
Section titled “3. Userinfo”Call userinfo with the access token as a Bearer token:
GET https://minutemail.co/idp/oauth/userinfoAuthorization: Bearer at_EXAMPLEACCESSTOKENResponse:
{ "sub": "ident_01JEXAMPLE", "email": "tricia.minutemail.cc@exampledomain.com", "email_verified": true, "name": "tricia", "preferred_username": "tricia", "picture": "https://example.com/avatar.png"}sub matches the ID token’s sub; email is the linked MinuteMail mailbox address. email_verified reflects the identity’s emailVerified field, and the identity’s custom claims are included alongside the standard profile fields. An invalid, expired, or missing token returns 401 with error: invalid_token.
Google, Apple, and custom clients use this endpoint. GitHub clients instead serve GitHub-shaped GET /user and GET /user/emails, and Facebook clients serve GET /me returning {id, name, email} — so your app’s provider adapter works unmodified. See GitHub and Facebook.
Scopes
Section titled “Scopes”| Scope | Grants |
|---|---|
openid | Issues an ID token at the token endpoint |
email | The email and email_verified claims |
profile | The name, preferred_username, and picture claims |
All three are granted by default when no scope is passed. There are no additional scopes to configure.
Notes for testing
Section titled “Notes for testing”- Access tokens are not refreshable — when one expires after an hour, restart the authorize flow to get a new one
- PKCE is strongly recommended even though a client secret is always required, so your integration exercises the same code path as a real public-client flow (GitHub and Facebook clients ignore PKCE parameters, like the real providers — see the Provider Behavior matrix)
- To test with a different user, deactivate the current one (
PATCH /v1/identities/{identityId}withisActive: false) or delete and recreate it — the consent screen always pre-selects the oldest active identity - To test both branches of your email-verification logic, toggle an identity’s
emailVerifiedfield — see Testing email verification and custom claims