Identity

On this page

Mock social login

Test "Sign in with Google", "Sign in with GitHub", "Sign in with Apple" and "Sign in with Microsoft" in CI without real provider accounts, phone verification or bot checks. A social login preset is a mock identity provider that speaks one provider's own wire protocol: its endpoint paths, parameters, claim names, token formats and error bodies. Common libraries work by changing only the endpoints or issuer.

Create one under Identity providers: pick a preset, give it a name. It starts with two sample users in the provider's style (a Google Workspace and a consumer account, a GitHub user with a private email, two Apple IDs, a work and a personal Microsoft account).

Presets are for testing. Anyone who can reach the account chooser can sign in as any test user, unless you protect the IdP. They are not affiliated with Google, GitHub, Apple or Microsoft.

How a sign-in works

  1. Your app redirects to the preset's authorize URL.
  2. An account chooser styled after the provider lists the test users. Under each one, Simulate picks a failure mode for this sign-in.
  3. The first time a user signs in to your app (or when new scopes or prompt=consent are requested), a consent screen follows: Google's permission list (with a checkbox for the email permission, like granular consent), GitHub's Authorize page, Apple's Share My Email / Hide My Email choice, Microsoft's Permissions requested. Cancel on either screen sends your app the provider's cancel error.
  4. The preset redirects (or, for Apple, posts) back with a code, which your app redeems at the token endpoint.

Consent is remembered per user. Revoke consent on the Users tab (or DELETE /api/idps/:id/users/:userId/consent) forgets it and invalidates every token issued to that user, as if they removed your app from their account.

For CI, set an auto-login user on the Setup tab: the authorize URL then answers straight away with a code, consent included. prompt=login and prompt=select_account still show the chooser.

Try sign-in runs a full flow against the built-in test app at https://testlie.com/idp/<slug>/callback, which is always an allowed redirect URI. It shows the callback parameters, the token response, the decoded ID token and the profile API answer.

Google

OIDC, with Google's endpoint layout under the preset's base URL https://testlie.com/idp/<slug> (also the issuer):

Endpoint Path
Discovery /.well-known/openid-configuration
Authorize /o/oauth2/v2/auth (also /o/oauth2/auth)
Token /token
Userinfo /oauth2/v3/userinfo (also /v1/userinfo)
JWKS /oauth2/v3/certs
Revoke /revoke
  • Scopes openid, email, profile or their URL forms (https://www.googleapis.com/auth/userinfo.email, …/userinfo.profile); other https://www.googleapis.com/auth/… scopes are granted and echoed. The token response lists the URL forms, like Google.
  • ID token and userinfo claims: sub (21 digits), email, email_verified, name, given_name, family_name, picture, locale, hd (Workspace users only), plus iss, azp, aud, at_hash, nonce. RS256.
  • The redirect carries code, scope, authuser and prompt. access_type=offline adds a refresh token (1//0…); refreshing does not return a new one. Access tokens look like ya29.a0…; expires_in is the lifetime minus one second.
  • prompt=none needs an auto-login user and existing consent (login_required, consent_required). PKCE S256 and plain. Token errors: invalid_grant with Bad Request, redirect_uri_mismatch, invalid_client.
  • /revoke with any token revokes the user's whole grant.
ts
// Auth.js
Google({ clientId, clientSecret, issuer: "https://testlie.com/idp/<slug>" })

// passport-google-oauth20
new GoogleStrategy({
  clientID, clientSecret, callbackURL,
  authorizationURL: "https://testlie.com/idp/<slug>/o/oauth2/v2/auth",
  tokenURL: "https://testlie.com/idp/<slug>/token",
  userProfileURL: "https://testlie.com/idp/<slug>/oauth2/v3/userinfo",
}, verify)

GitHub

Plain OAuth 2.0 (no OIDC, no discovery), with GitHub's paths:

Endpoint Path
Authorize /login/oauth/authorize
Token POST /login/oauth/access_token
User GET /user (also /api/v3/user)
Emails GET /user/emails (also /api/v3/user/emails)
Revoke DELETE /applications/{client_id}/grant and /token (Basic client auth, JSON {"access_token": "…"})
  • Scopes are comma- or space-separated (read:user,user:email). redirect_uri defaults to the first registered one. PKCE is supported. login pre-selects an account.
  • The token endpoint answers in the format the Accept header asks for: application/x-www-form-urlencoded by default (access_token=gho_…&scope=read%3Auser%2Cuser%3Aemail&token_type=bearer), JSON for application/json, XML for application/xml. Errors come back with HTTP 200 and bad_verification_code, incorrect_client_credentials, redirect_uri_mismatch or bad_refresh_token, as on GitHub.
  • GET /user (with Authorization: token … or Bearer …) returns the REST API's authenticated-user object: login, numeric id, node_id, avatar_url, html_url, name, company, email (null for a private email), created_at and the rest, with X-OAuth-Scopes and rate-limit headers.
  • GET /user/emails lists the primary address (verified, visibility) and the …@users.noreply.github.com address. Without user:email (or user) it answers 404, like GitHub.
  • Bad tokens get 401 {"message": "Bad credentials"}.
  • Expiring user tokens (preset setting) switch to GitHub App style: ghu_ tokens for 8 hours with rotating ghr_ refresh tokens.
ts
// Auth.js
GitHub({ clientId, clientSecret, enterprise: { baseUrl: "https://testlie.com/idp/<slug>" } })

// passport-github2
new GitHubStrategy({
  clientID, clientSecret, callbackURL, scope: ["user:email"],
  authorizationURL: "https://testlie.com/idp/<slug>/login/oauth/authorize",
  tokenURL: "https://testlie.com/idp/<slug>/login/oauth/access_token",
  userProfileURL: "https://testlie.com/idp/<slug>/user",
  userEmailURL: "https://testlie.com/idp/<slug>/user/emails",
}, verify)

Apple

OIDC with Apple's paths: discovery at /.well-known/openid-configuration, /auth/authorize, /auth/token, /auth/keys (JWKS), /auth/revoke.

  • Scopes name and email. Requesting either requires response_mode=form_post (otherwise invalid_request). response_type is code or code id_token.
  • The callback is an auto-submitted POST with state, code, id_token (for code id_token, with c_hash) and, on the first authorization only, user: {"name":{"firstName":"…","lastName":"…"},"email":"…"}. Revoking consent makes the next sign-in send it again.
  • Hide My Email: relay addresses <id>@privaterelay.appleid.com, stable per user and app, with is_private_email: true. The preset setting lets the user choose (default), or always hides or always shares.
  • ID token claims: iss, aud, sub (000123.<hex>.0456), email, email_verified, is_private_email, auth_time, nonce, nonce_supported, real_user_status, at_hash / c_hash. Signed RS256 like Apple; switch the preset to ES256 to sign with the EC P-256 key. The JWKS always publishes both keys.
  • client_secret may be your real Sign in with Apple client secret: an ES256 JWT with iss your team id, sub the client id, aud https://appleid.apple.com (or the preset's issuer), at most 6 months between iat and exp. Paste the public key of your .p8 key into the preset settings to have signatures verified too. The preset's own client secret is also accepted. Errors: invalid_client, invalid_grant.
  • Every token response has a refresh token; refreshing returns a new access and ID token only. /auth/revoke ends the grant.
  • Sign in with Apple JS always talks to appleid.apple.com, so in tests link your button to the preset's authorize URL with the same parameters (response_type=code id_token, response_mode=form_post, state, nonce); your backend's form_post handler stays the same.
ts
// Auth.js
Apple({ clientId, clientSecret, issuer: "https://testlie.com/idp/<slug>" })

Microsoft

Microsoft identity platform v2.0 endpoints, with the tenant segment:

Endpoint Path
Discovery /<tenant>/v2.0/.well-known/openid-configuration
Authorize /<tenant>/oauth2/v2.0/authorize
Token /<tenant>/oauth2/v2.0/token
JWKS /<tenant>/discovery/v2.0/keys
Logout /<tenant>/oauth2/v2.0/logout
Graph /v1.0/me, OIDC userinfo /oidc/userinfo
  • <tenant> is common (all accounts), organizations (work and school only), consumers (personal only), a tenant id or a domain. Accounts a tenant does not allow are greyed out on the chooser.
  • Test users are work accounts in the preset's tenant (or their own tenantId) or personal accounts in the consumers tenant 9188040d-6c67-4c5b-b112-36a304b66dad.
  • ID tokens (ver: "2.0") have iss https://testlie.com/idp/<slug>/<tid>/v2.0, tid, oid, pairwise sub, preferred_username, name, email (with the email scope), nonce, aio, rh, uti. The common and organizations discovery documents publish the {tenantid} issuer template, like Microsoft.
  • client_info=1 adds MSAL's client_info to the token response. Responses include ext_expires_in; offline_access adds refresh tokens, which rotate. Access tokens are JWTs for Graph.
  • Errors use Entra's body (error, error_description starting with AADSTS…, error_codes, trace_id, correlation_id): 54005 code already redeemed, 70008 expired code, 50148 PKCE, 7000215 bad secret, 65001 consent revoked (suberror: consent_required). Cancel is access_denied with error_subcode=cancel (AADSTS65004). Graph answers bad tokens with InvalidAuthenticationToken.
ts
// MSAL Node (authorities must be https: use your hosted URL or a TLS proxy locally)
new ConfidentialClientApplication({
  auth: { clientId, clientSecret, authority: "https://testlie.com/idp/<slug>/common", knownAuthorities: ["<your host>"] },
})

// Auth.js
MicrosoftEntraID({ clientId, clientSecret, issuer: "https://testlie.com/idp/<slug>/<tenant id>/v2.0" })

Failure modes

Pick one per sign-in under Simulate on the chooser, add ?fault=<id> to the authorize URL (?fault=none turns a default off), or set a default on the Setup tab. Reusing a code always fails with the provider's own error, fault or not.

Fault What happens Providers
user_cancel The callback carries the cancel error: access_denied (GitHub with its description, Microsoft with error_subcode=cancel), Apple user_cancelled_authorize all
code_reuse The first redemption of the code fails as already redeemed: invalid_grant, GitHub bad_verification_code, Microsoft AADSTS54005 all
expired_access_token The token response looks normal, but the profile API answers 401 (userinfo, GET /user, Graph /me) Google, GitHub, Microsoft
expired_id_token The ID token's exp is in the past Google, Apple, Microsoft
consent_revoked The user revoked your app right after signing in: profile API 401, refresh token rejected (Microsoft AADSTS65001), consent asked again next time all
email_unverified email_verified: false (GitHub: verified: false on /user/emails; Microsoft: xms_edov: false) all
missing_email The email permission is not granted: no email claim, no email scope in the token response; GitHub /user email null and /user/emails 404 all

Provider profile of a test user

Each test user has a Provider profile (JSON) next to its email, name and extra claims. Every field is optional:

Field Used by Meaning
picture Google, Microsoft, GitHub Picture / avatar_url; defaults to a generated avatar on the preset
hd Google Workspace domain (hd claim); leave out for consumer accounts
emailVerified all false reports the email as unverified on every sign-in
locale Google locale claim
login, githubId GitHub Login and numeric id (the id defaults to the subject)
emailPublic GitHub false: a private email, null on GET /user
company, location, bio GitHub Profile fields
realUserStatus Apple 0, 1 or 2
accountType, tenantId Microsoft work or personal; tenant of a work account

Subjects are generated in the provider's format when you leave them empty.

API

Endpoint Description
POST /api/idps { "name", "kind": "google" | "github" | "apple" | "microsoft", "redirectUris"?, "social"? }
PATCH /api/idps/:id social: { defaultFault, appName, alwaysConsent, relayEmails, idTokenAlg, appleClientKey, tenantId, expiringTokens } (merged)
POST / PATCH /api/idps/:id/users[/:userId] Also take profile
DELETE /api/idps/:id/users/:userId/consent Revoke the user's consent and tokens

IdP responses include kind and social (settings plus every endpoint URL).