ErtisAuth

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.

flow
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#

typeWhat the client sendsHow ErtisAuth verifies it
GoogleThe Google ID tokenValidates the token's signature and audience with Google
FacebookThe Facebook access tokenAsks the Facebook Graph API about the token and reads the profile from it
Facebook with Limited LoginThe Limited Login JWTValidates the JWT with Facebook's keys
MicrosoftA Microsoft access token for Microsoft GraphReads the profile from Microsoft Graph with the token
AppleThe authorization code of Sign in with Apple on the webExchanges the code with Apple and reads the identity from Apple's ID token
AppleNativeThe authorization code of Sign in with Apple in a native iOS/macOS appSame 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#

json
{
	"_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"
}
FieldRequiredDescription
typeyesGoogle, Facebook, Microsoft, Apple or AppleNative. Can't be changed later.
namenoDisplay name. The type by default.
slugnoUnique 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.
descriptionno
isActivenoInactive providers reject sign-ins (403 ProviderIsDisable). false by default.
defaultRolewhen activeRole slug given to users created by this provider.
defaultUserTypewhen activeUser type slug given to users created by this provider.
appClientIdwhen activeThe 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).
tenantIdnoMicrosoft: your Microsoft Entra tenant id.
teamId, privateKeyId, privateKeyApple typesYour Apple Developer team id, the key id of a Sign in with Apple key, and the key itself (the contents of the .p8 file).
redirectUriApple typesThe redirect URI registered for your Services ID, the same as the one used by the client.
trust_emailnoSee 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#

http
POST /oauth/{slug}/login
X-Ertis-Alias: <membership_id>
Content-Type: application/json
HeaderRequiredDescription
X-Ertis-AliasyesThe membership id
X-IpAddress, X-UserAgentnoStored with the session, as for password sign-in

No token is needed. The body depends on the provider's type.

Google#

json
{
	"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#

json
{
	"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#

json
{
	"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#

json
{
	"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#

StatusCodeWhen
400MembershipIdRequiredX-Ertis-Alias is missing
400InvalidProviderLoginRequestThe body is not a valid login result for the provider's type
401UnauthorizedThe provider did not accept the token or code
401ProviderProfileIncompleteThe provider profile lacks the email address or the name
401UserInactiveThe matching user is inactive or frozen
403ProviderNotConfiguredNo provider with this slug
403ProviderIsDisableThe provider is not active
403UntrustedProviderThe client id in the request is not the provider's client id
409ProviderEmailNotTrustedA user with the same email exists, and the email can't be trusted (see below)
501ProviderNotConfiguredCorrectlyThe provider configuration is incomplete or wrong (e.g. an unreadable Apple key)
503ProviderUnavailableThe provider could not be reached; try again later

How users are matched#

  1. By connected account. Every user keeps the accounts they signed in with in connected_accounts:
    json
    "connected_accounts": [
    	{ "provider": "Google", "slug": "google", "user_id": "109876543210987654321" }
    ]
    A sign-in finds the user whose connected account has the same provider type and provider user id.
  2. 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.
  3. Sign-up. If no user matches, a new user is created with the provider's defaultRole and defaultUserType, the name and email from the provider, the email address as username, and source_provider set 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.

ProviderLinked by email
Googlewhen Google reports the email as verified
Apple, AppleNativewhen Apple reports the email as verified
Facebookonly when the provider has trust_email: true
Microsoftonly 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.

Warning: set 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}.

MethodRouteDescriptionPermission
GET/providers/{id}Get a provider (id or slug)providers.read.{id}
GET/providersList the providers of the membershipproviders.read
GET/providers/active-providersPublic settings of the active providersnone
POST/providersCreate a providerproviders.create
PUT/providers/{id}Update a providerproviders.update.{id}
DELETE/providers/{id}Delete a providerproviders.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:

json
[
	{ "_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#

shell
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.

ErrorWhen
400 ProviderTypeRequired, 400 UnknownProvider, 400 UnsupportedProviderMissing or unknown type
400 ModelValidationErrorAn active provider misses a required setting
409 ProviderAlreadyExistsThe slug is taken

Update a provider#

http
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#

http
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