External Identity Providers
Google, Apple, Facebook and Microsoft sign-in.
Users can sign in with an account they already have at Google, Apple, Facebook or Microsoft. Your client app runs the provider's own sign-in (its SDK or web flow) and sends the result to ErtisAuth, which verifies it with the provider and returns an ErtisAuth token pair, exactly like a password sign-in.
Your app ──(1) provider sign-in──▶ Google / Apple / Facebook / Microsoft
│ │
│◀──────────(2) provider token ────────────┘
│
└──(3) POST /oauth/{slug}/login ──▶ ErtisAuth ──(4) verify──▶ provider
│
◀──────────(5) ErtisAuth tokens ───────┘On the first sign-in the user is created; later sign-ins find the same user again.
Supported providers#
type | What the client sends | How ErtisAuth verifies it |
|---|---|---|
Google | The Google ID token | Validates the token's signature and audience with Google |
Facebook | The Facebook access token | Asks the Facebook Graph API about the token and reads the profile from it |
Facebook with Limited Login | The Limited Login JWT | Validates the JWT with Facebook's keys |
Microsoft | A Microsoft access token for Microsoft Graph | Reads the profile from Microsoft Graph with the token |
Apple | The authorization code of Sign in with Apple on the web | Exchanges the code with Apple and reads the identity from Apple's ID token |
AppleNative | The authorization code of Sign in with Apple in a native iOS/macOS app | Same as Apple, with the app's bundle id as client id |
The identity (provider user id, email address) is always taken from the data ErtisAuth gets from the provider, never from what the client claims. Names sent by the client are used only where the provider shares them with the client alone (Apple sends the user's name only once, to the app).
The provider object#
{
"_id": "66f1c0d2a4b5c6d7e8f90180",
"type": "Google",
"name": "Google",
"slug": "google",
"description": "Sign in with Google for the web app",
"defaultRole": "customer",
"defaultUserType": "customer",
"appClientId": "1234567890-abc.apps.googleusercontent.com",
"isActive": true,
"trust_email": false,
"membership_id": "66f1c0d2a4b5c6d7e8f90123"
}| Field | Required | Description |
|---|---|---|
type | yes | Google, Facebook, Microsoft, Apple or AppleNative. Can't be changed later. |
name | no | Display name. The type by default. |
slug | no | Unique in the membership; the name by default. Can't be changed later: it is the provider's login URL (/oauth/{slug}/login) and is stored in the users' connected accounts. |
description | no | |
isActive | no | Inactive providers reject sign-ins (403 ProviderIsDisable). false by default. |
defaultRole | when active | Role slug given to users created by this provider. |
defaultUserType | when active | User type slug given to users created by this provider. |
appClientId | when active | The client id of your app at the provider: the Google OAuth client id, the Facebook app id, the Microsoft application (client) id, or the Apple Services ID (Apple) / bundle id (AppleNative). |
tenantId | no | Microsoft: your Microsoft Entra tenant id. |
teamId, privateKeyId, privateKey | Apple types | Your Apple Developer team id, the key id of a Sign in with Apple key, and the key itself (the contents of the .p8 file). |
redirectUri | Apple types | The redirect URI registered for your Services ID, the same as the one used by the client. |
trust_email | no | See Linking accounts. false by default. |
You can create several providers of the same type, for example one Google provider per app with its own client id, each with its own slug.
The default user type should allow the fields the provider fills in (firstname, lastname, email_address, username). If it declares an object field named avatar, the provider's profile picture URL is stored in avatar.url.
Signing in#
POST /oauth/{slug}/login
X-Ertis-Alias: <membership_id>
Content-Type: application/json| Header | Required | Description |
|---|---|---|
X-Ertis-Alias | yes | The membership id |
X-IpAddress, X-UserAgent | no | Stored with the session, as for password sign-in |
No token is needed. The body depends on the provider's type.
Google#
{
"clientId": "1234567890-abc.apps.googleusercontent.com",
"token": {
"idToken": "eyJhbGciOiJSUzI1NiIs…"
}
}The ID token must be issued for the provider's appClientId, and it must contain the user's email and name: request the openid email profile scopes. A token without them answers 401 ProviderProfileIncomplete.
Facebook#
{
"appId": "<facebook_app_id>",
"user": {
"id": "<facebook_user_id>",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@example.com",
"accessToken": "<facebook_access_token>"
}
}appId must be the provider's appClientId. id, first_name, email and accessToken are required in the body, but the identity and the email address that ErtisAuth uses are read from Facebook with the access token. For Facebook Limited Login (iOS), add ?limited_flow=true to the URL and send the Limited Login authentication token as accessToken.
Microsoft#
{
"clientId": "<application_client_id>",
"token": {
"accessToken": "<access_token_for_microsoft_graph>"
}
}The access token must be valid for Microsoft Graph (User.Read).
Apple and AppleNative#
{
"authorization": {
"code": "<authorization_code>",
"id_token": "<id_token>"
},
"user": {
"name": { "firstName": "Ada", "lastName": "Lovelace" },
"email": "ada@example.com"
}
}This is the response of Sign in with Apple, sent as is. user is only present on the user's very first sign-in, when Apple shares the name with the app; send it when you have it. ErtisAuth exchanges code with Apple, so a code can be used only once and expires after a few minutes.
Response#
201 Created with an ErtisAuth token pair, as for a password sign-in.
Errors#
| Status | Code | When |
|---|---|---|
400 | MembershipIdRequired | X-Ertis-Alias is missing |
400 | InvalidProviderLoginRequest | The body is not a valid login result for the provider's type |
401 | Unauthorized | The provider did not accept the token or code |
401 | ProviderProfileIncomplete | The provider profile lacks the email address or the name |
401 | UserInactive | The matching user is inactive or frozen |
403 | ProviderNotConfigured | No provider with this slug |
403 | ProviderIsDisable | The provider is not active |
403 | UntrustedProvider | The client id in the request is not the provider's client id |
409 | ProviderEmailNotTrusted | A user with the same email exists, and the email can't be trusted (see below) |
501 | ProviderNotConfiguredCorrectly | The provider configuration is incomplete or wrong (e.g. an unreadable Apple key) |
503 | ProviderUnavailable | The provider could not be reached; try again later |
How users are matched#
- By connected account. Every user keeps the accounts they signed in with in
connected_accounts:A sign-in finds the user whose connected account has the same provider type and provider user id."connected_accounts": [ { "provider": "Google", "slug": "google", "user_id": "109876543210987654321" } ] - By email address. If no connected account matches, ErtisAuth looks for a user with the same email address in the membership and links the provider account to it, see below.
- Sign-up. If no user matches, a new user is created with the provider's
defaultRoleanddefaultUserType, the name and email from the provider, the email address asusername, andsource_providerset to the provider type. It has no password and signs in through the provider.
Every sign-in records a TokenGenerated event; a sign-up also records UserCreated.
Linking to existing accounts#
Linking a provider account to an existing user by email address is only safe if the provider guarantees that the user owns that address. Otherwise anyone could create a provider account with someone else's email address and take over their ErtisAuth account.
| Provider | Linked by email |
|---|---|
| when Google reports the email as verified | |
| Apple, AppleNative | when Apple reports the email as verified |
only when the provider has trust_email: true | |
| Microsoft | only when the provider has trust_email: true |
When an existing user has the same email address and the email can't be trusted, the sign-in is rejected with 409 ProviderEmailNotTrusted. The user can sign in with their password and you can then let them connect the account.
trust_email to true only if you accept that the provider may not have verified the email address. For Microsoft, the mail attribute is managed by the user's organization and is not verified by Microsoft.Account activation#
When the membership requires activation, users created by a provider sign-up are created inactive like any other user, and they receive the activation mail if the hook is set up.
Signing out#
Revoking an ErtisAuth token also revokes, where the provider supports it, the provider token that was stored with the user's connected account.
Managing providers#
All routes are under /memberships/{membershipId}.
| Method | Route | Description | Permission |
|---|---|---|---|
GET | /providers/{id} | Get a provider (id or slug) | providers.read.{id} |
GET | /providers | List the providers of the membership | providers.read |
GET | /providers/active-providers | Public settings of the active providers | none |
POST | /providers | Create a provider | providers.create |
PUT | /providers/{id} | Update a provider | providers.update.{id} |
DELETE | /providers/{id} | Delete a provider | providers.delete.{id} |
Active providers#
GET /memberships/{membershipId}/providers/active-providers is public, so that sign-in pages can show the right buttons. It returns only the public settings:
[
{ "_id": "…", "name": "Google", "slug": "google", "type": "Google", "appClientId": "1234567890-abc.apps.googleusercontent.com", "membership_id": "…" },
{ "_id": "…", "name": "Apple", "slug": "apple", "type": "Apple", "appClientId": "com.example.web", "redirectUri": "https://app.example.com/auth/apple", "membership_id": "…" },
{ "_id": "…", "name": "Microsoft", "slug": "microsoft", "type": "Microsoft", "appClientId": "…", "tenantId": "…", "membership_id": "…" }
]Create a provider#
curl -X POST https://auth.example.com/memberships/<membership_id>/providers \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{
"type": "Apple",
"name": "Sign in with Apple",
"slug": "apple",
"defaultRole": "customer",
"defaultUserType": "customer",
"appClientId": "com.example.web",
"teamId": "ABCDE12345",
"privateKeyId": "XYZ987WVU6",
"privateKey": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMG…\n-----END PRIVATE KEY-----",
"redirectUri": "https://app.example.com/auth/apple",
"isActive": true
}'Response 201 Created: the provider.
| Error | When |
|---|---|
400 ProviderTypeRequired, 400 UnknownProvider, 400 UnsupportedProvider | Missing or unknown type |
400 ModelValidationError | An active provider misses a required setting |
409 ProviderAlreadyExists | The slug is taken |
Update a provider#
PUT /memberships/{membershipId}/providers/{id}Omitted fields keep their current values. type can't change, and a different slug answers 400 ProviderSlugCannotBeChanged. An update without any change answers 409 IdenticalDocumentError.
Delete a provider#
DELETE /memberships/{membershipId}/providers/{id}Response 204 No Content. Users created by it keep their accounts.
Events#
ProviderCreated, ProviderUpdated and ProviderDeleted. See Events.
Found a mistake in the docs? Open an issue