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
- Your app redirects to the preset's authorize URL.
- An account chooser styled after the provider lists the test users. Under each one, Simulate picks a failure mode for this sign-in.
- The first time a user signs in to your app (or when new scopes or
prompt=consentare 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. - 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.
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,profileor their URL forms (https://www.googleapis.com/auth/userinfo.email,…/userinfo.profile); otherhttps://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), plusiss,azp,aud,at_hash,nonce. RS256. - The redirect carries
code,scope,authuserandprompt.access_type=offlineadds a refresh token (1//0…); refreshing does not return a new one. Access tokens look likeya29.a0…;expires_inis the lifetime minus one second. prompt=noneneeds an auto-login user and existing consent (login_required,consent_required). PKCES256andplain. Token errors:invalid_grantwithBad Request,redirect_uri_mismatch,invalid_client./revokewith any token revokes the user's whole grant.
// 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_uridefaults to the first registered one. PKCE is supported.loginpre-selects an account. - The token endpoint answers in the format the
Acceptheader asks for:application/x-www-form-urlencodedby default (access_token=gho_…&scope=read%3Auser%2Cuser%3Aemail&token_type=bearer), JSON forapplication/json, XML forapplication/xml. Errors come back with HTTP 200 andbad_verification_code,incorrect_client_credentials,redirect_uri_mismatchorbad_refresh_token, as on GitHub. GET /user(withAuthorization: token …orBearer …) returns the REST API's authenticated-user object:login, numericid,node_id,avatar_url,html_url,name,company,email(null for a private email),created_atand the rest, withX-OAuth-Scopesand rate-limit headers.GET /user/emailslists the primary address (verified,visibility) and the…@users.noreply.github.comaddress. Withoutuser:email(oruser) 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 rotatingghr_refresh tokens.
// 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
nameandemail. Requesting either requiresresponse_mode=form_post(otherwiseinvalid_request).response_typeiscodeorcode id_token. - The callback is an auto-submitted POST with
state,code,id_token(forcode id_token, withc_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, withis_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_secretmay be your real Sign in with Apple client secret: an ES256 JWT withissyour team id,subthe client id,audhttps://appleid.apple.com(or the preset's issuer), at most 6 months betweeniatandexp. Paste the public key of your.p8key 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/revokeends 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.
// 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>iscommon(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 tenant9188040d-6c67-4c5b-b112-36a304b66dad. - ID tokens (
ver: "2.0") haveisshttps://testlie.com/idp/<slug>/<tid>/v2.0,tid,oid, pairwisesub,preferred_username,name,email(with theemailscope),nonce,aio,rh,uti. The common and organizations discovery documents publish the{tenantid}issuer template, like Microsoft. client_info=1adds MSAL'sclient_infoto the token response. Responses includeext_expires_in;offline_accessadds refresh tokens, which rotate. Access tokens are JWTs for Graph.- Errors use Entra's body (
error,error_descriptionstarting withAADSTS…,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 isaccess_deniedwitherror_subcode=cancel(AADSTS65004). Graph answers bad tokens withInvalidAuthenticationToken.
// 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 |
Workspace domain (hd claim); leave out for consumer accounts |
|
emailVerified |
all | false reports the email as unverified on every sign-in |
locale |
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).