Skip to content

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:

EndpointMethodPurpose
/idp/oauth/authorizeGETShow the consent screen and issue an authorization code
/idp/oauth/tokenPOSTExchange an authorization code for an access token
/idp/oauth/userinfoGETFetch the mock identity’s profile with an access token
/idp/jwksGETPublic RSA keys for verifying ID token signatures
/idp/.well-known/openid-configurationGETOIDC discovery document

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
ParameterRequiredDescription
client_idYesClient ID from the dashboard
redirect_uriYesMust exactly match one of the client’s registered redirect URIs
response_typeConventionally code; the flow is always authorization code
scopeNoSpace-separated scopes. Supported: openid, email, profile. Defaults to all three if omitted
stateRecommendedOpaque value echoed back on redirect; use it for CSRF protection
code_challengeRecommendedPKCE challenge (S256), for public clients
code_challenge_methodWith code_challengeMust be S256

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_uri with the code (Apple clients instead deliver the response as an auto-submitting form POST — see Apple):
HTTP/1.1 302 Found
Location: http://localhost:3000/callback?code=ac_EXAMPLECODE&state=xyz123
  • Deny — the redirect carries an error instead, with your state preserved:
Location: http://localhost:3000/callback?error=access_denied&error_description=The%20user%20denied%20the%20authorization%20request.&state=xyz123

Authorization 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

Exchange the code server-to-server with a form-encoded POST:

POST https://minutemail.co/idp/oauth/token
Content-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
ParameterRequiredDescription
grant_typeYesMust be authorization_code
codeYesThe authorization code from the redirect
client_idYesThe client’s ID
client_secretYesThe client’s secret — verified on every exchange, including PKCE clients
redirect_uriYesThe same redirect_uri used in the authorize request
code_verifierIf PKCE was usedThe 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..."
}
FieldDescription
access_tokenOpaque Bearer token, valid for 1 hour
token_typeBearer — except GitHub and Facebook clients, which return "bearer" (lowercase) like the real providers
expires_inLifetime in seconds (3600) — omitted for GitHub clients, whose real-world app tokens don’t expire
scopeThe scopes granted
id_tokenPresent 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):

ClaimValue
issThe issuer URL
subThe mock identity’s ID
audYour client_id
emailThe linked mailbox address
email_verifiedThe 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, pictureThe 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
nonceEchoed from the authorization request
custom claimsThe 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).

Errors use the standard OAuth error format with Cache-Control: no-store:

{
"error": "invalid_grant",
"error_description": "Authorization code has expired."
}
HTTPerrorCause
400invalid_requestMissing code or client_id, or malformed form data
400unsupported_grant_typegrant_type is not authorization_code
400invalid_grantCode not found, expired, already used, or redirect_uri mismatch
401invalid_clientBad client_id/client_secret, or PKCE verification failed
500server_errorUnexpected server error

Call userinfo with the access token as a Bearer token:

GET https://minutemail.co/idp/oauth/userinfo
Authorization: Bearer at_EXAMPLEACCESSTOKEN

Response:

{
"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.

ScopeGrants
openidIssues an ID token at the token endpoint
emailThe email and email_verified claims
profileThe name, preferred_username, and picture claims

All three are granted by default when no scope is passed. There are no additional scopes to configure.

  • 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} with isActive: 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 emailVerified field — see Testing email verification and custom claims