Google Cloud Identity Platform Blocking Functions
Google Cloud's Identity Platform (GCIP)
aims to provide Developers with Google-grade Authentication and Security for
their applications, services, APIs, or anything else that requires identity and
authentication.
Identity Platform allows you to trigger Cloud Functions synchronously (blocking
flow) in response to the following authentication events
- Before user account creation
- Before user sign-in completion
These triggers block the underlying authentication events from completing and
allows you to customize these events using your custom code in Cloud Functions.
They are different from the asynchronous pub/sub-modeled OnCreate
and
OnDelete
triggers (integrated with
Cloud Functions for Firebase)
that execute after a user account has been created or deleted and doesn't block
the underlying authentication event.
Blocking Functions
enable the following capabilities
- Block user sign-up or sign-in from succeeding if it doesn't meet certain
criteria (example from a banned email domain, certain IP addresses etc.)
- Update user profile information (
displayName
, photoURL
, disabled
,
emailVerified
, etc).
- Modify or define persistent or session-specific custom claims on a user.
Table of Contents
Before you start
To use blocking functions (beforeCreate and beforeSignIn) with Google Cloud
Identity Platform:
-
Create a project on Google Cloud Console and enable
Identity Platform.
-
Enable the sign-in providers you want to support through Identity Platform.
The following providers are supported for blocking functions:
- Email/password and Email link
- Social: Google, Facebook, Apple, Twitter, GitHub, Microsoft, Yahoo,
LinkedIn
- Games: Google Play Games + Game Center for iOS
- SAML
- OIDC
- Phone authentication
- Multi-factor authentication with SMS. This will only trigger
beforeSignIn
events.
-
Deploy an HTTP trigger for the blocking function
-
Using the Cloud Console
- Go to Cloud Functions
- Select Create Function
- Give the function a name
- Select a GCP region.
- Select an HTTP trigger type.
- The HTTP trigger should allow unauthenticated invocations so that
Identity Platform server can access it.
- Click Save
- Click Next to write the function.
- Click Deploy to deploy the newly created trigger.
-
Using gcloud CLI
Install gcloud SDK, if you have
not already done so.
Initialize gcloud via command line:
gcloud init
gcloud components update
Follow the instructions below on how to
write an authentication blocking function.
gcloud functions deploy $before_create_func_name --runtime nodejs10 --trigger-http --allow-unauthenticated
gcloud functions deploy $before_signin_func_name --runtime nodejs10 --trigger-http --allow-unauthenticated
$before_create_func_name
and $before_signin_func_name
are the
corresponding function names. In the example below,
$before_create_func_name
would be myBeforeCreateFunc
.
exports.myBeforeCreateFunc = auth.functions().beforeCreateHandler((user, context) => {
});
Learn more about
GCF HTTP triggers.
-
Register the blocking function triggers with Identity Platform
-
In the Identity Platform Cloud Console section
-
Go to the Settings menu
-
In the Triggers tab, click the drop down menu for the relevant event
(beforeCreate
, beforeSignin
or both) for which you want to trigger your
cloud function.
-
Select the HTTP trigger previously deployed.
-
If no function is deployed yet, click on Create Function
. This will
redirect you to Google Cloud Functions and and allows you to set up an
HTTP trigger.
The GCF Cloud Console UI is convenient for simple functions.
For more complicated functions that require access to a code editor,
consider using the gcloud SDK. After deploying the HTTP trigger with GCF,
go back to the Identity Platform Triggers section and select the newly
deployed trigger from the event drop down menu.
-
Optional: To forward additional inbound IdP credentials (IdP access tokens
or refresh tokens, etc), expand the Include token credentials section
and select the credentials to forward. For example, to forward the Google
ID token, access token and refresh token of a signed in user, check the
boxes for ID token, Access token and Refresh token. By doing
so, the function will receive these IdP credentials. This setting applies
to both events. The following OAuth credentials can be forwarded:
- ID token: Available for OIDC providers or social providers that are
OIDC compliant.
- Access token: Available for OAuth 2.0 providers or OIDC providers
with code flows enabled. This also refers to the Twitter OAuth access
token used in addition with the token secret.
- Refresh token: Available for OAuth 2.0 providers or OIDC providers
with code flows enabled.
-
Click Save to complete.
Warning: Deleting a GCF HTTP trigger registered with Identity Platform
without first unregistering it (setting the trigger to None) in the Identity
Platform Cloud Console UI will result in all users failing to sign in or sign
up to your application.
Blocking functions general behavior
- When triggered, your function should respond within 7 seconds. After this
time, Identity Platform will timeout and return an error.
- A non-200 HTTP response from your function will result in the underlying
authentication event failing and the error propagating to the client.
Visit blocking function default error codes to learn more.
- If you delete the underlying function in Cloud Functions, you must update
the trigger in Identity Platform and set it to none. Failure to do so will
result in an error propagating to the client.
- Note that Identity Platform will trigger these functions for all the users
in your project. If you have enabled multi-tenancy in your project and are
authenticating users for that tenant, the blocking functions will be
triggered for these tenant users as well. Identity Platform will pass on
tenant information to your function, which you can use in your code and apply
appropriate logic.
Writing blocking functions
gcip-cloud-functions
module is provided to help verify the incoming request,
parse the payload, and return the response to the Auth server in the right
format. The only requirement is to provide a callback function which takes the
user record and context information and returns either an object of user
properties/custom claims that need to be modified or throw an error to force
the sign-in or sign-up operation to fail
Before you begin
Refer to the cloud functions documentation on
how to write cloud functions.
The provided code needs to be structured following the GCF
requirement.
In the package.json
file, the gcip-cloud-functions
npm module needs to be
provided:
"dependencies": {
"gcip-cloud-functions": "^0.0.1"
}
If installing via CLI, this can also be done via command line:
npm install gcip-cloud-functions --save
In the index.js
(the file used to export the functions), the
gcip-cloud-functions
module needs to be required.
Using commonjs:
const gcipCloudFunctions = require('gcip-cloud-functions');
Using ES6 imports:
import * as gcipCloudFunctions from 'gcip-cloud-functions';
When initializing an Auth instance for usage with blocking functions,
the projectId
is needed to ensure only events targeting this project are
allowed. The projectId
can be explicitly specified but since the code will be
running in GCP infrastructure, the library will be able to auto-discover it by
calling the GCE metadata server internal. This is the recommended process to
initialize an Auth client.
const authClient = new gcipCloudFunctions.Auth();
If there is a need to manually provide the project ID, you can also specify it
via environment variable GCP_PROJECT
during deployment. In gcloud, this is
done via --set-env-vars
flag. If deploying the function via the Cloud Console
UI, the environment variables can be set in the
Variables, networking and advanced settings menu.
gcloud functions deploy $before_create_func_name --runtime nodejs10 \
--trigger-http --allow-unauthenticated --set-env-vars GCP_PROJECT=$project_id
BeforeCreate event
Event trigger
This event is triggered when a new user attempts to sign up (credential
verified or validated) and right before the user is saved in the Auth database
and the ID token and refresh tokens are returned to the client.
You can create a function that triggers before a user is created using the
Auth#functions().beforeCreateHandler()
event handler:
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
});
Before a user creation event completes, the event handler callback will trigger
with a
UserRecord
object, identifying the user about to be created, and an
extended
EventContext
object.
When user creation is allowed, the callback is expected to return a response
(synchronous or asynchronous via Promise) with the optional attributes of the
user to be modified on the newly created user. If nothing is returned, the
operation will succeed without modifying the user.
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
return {
displayName: user.displayName || 'Guest';
};
});
When the operation is disallowed, raise an HttpsError
. This will surface to
the client API.
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
if (!isAuthorizedEmail(user.email)) {
throw new gcipCloudFunctions.https.HttpsError(
'invalid-argument', `Unauthorized email "${user.email}"`);
}
});
Users created via Admin SDK (Authenticated REST API), CLI or Cloud Console will
not trigger the beforeCreate
event. Only users created via the client API will
trigger this event.
Response fields that can be modified
Only modification of the following fields is allowed: displayName
,
photoURL
, customClaims
, emailVerified
, disabled
. Note that
sessionClaims
are not supported in beforeCreate
. To set sessionClaims
,
beforeSignIn
needs to be used in addition to beforeCreate
.
Supported sign-up methods:
- Email link
- Email/Password
- Federated sign in (OAuth, OIDC, SAML))
- phone number sign-in
- Game Center sign-in
displayName | string | Persisted in database and propagated to ID token ("name" claim). Propagated to beforeSignIn event if available. |
disabled | boolean | Persisted in the database. This signals that the account is created in disabled mode. Client should throw USER_DISABLED error and account set as disabled. No beforeSignIn event should be triggered. |
emailVerified | boolean | Persisted in database and propagated to ID token ("email_verified" claim). Propagated to beforeSignIn event if available. Note that emailVerified is not propagated to the ID token if no email is set on the account. |
photoURL | string (URL) | Persisted in database and propagated to ID token ("picture" claim).. Propagated to beforeSignIn event if available. |
customClaims | Object with 1K byte size limit and no reserved OIDC claim names. | Persisted in the database and propagated to the ID token. Stored as single value (no delta modifications) and also propagated to beforeSignIn event if available. If beforeSignIn returns customClaims, they will completely overwrite customClaims from beforeCreate. If beforeSignIn returns sessionClaims, the beforeSignIn claims will be merged with beforeCreate claims and beforeSignIn claims will overwrite overlapping claims. Example 1: beforeCreate returns {a: 1, b: 2, e: 0} custom claims beforeSignIn returns {c: 3, d: 4, e: 5} session claims In the Auth database, the user record will have {a: 1, b: 2, e: 0} as custom claims. In the user's ID token, the following claims will be available: {a: 1, b: 2, c: 3, d: 4, e: 5} Note that refreshing the user's token will preserve these claims: {a: 1, b: 2, c: 3, d: 4, e: 5} Example 2: beforeCreate returns {a: 1, b: 2, e: 0} custom claims beforeSignIn returns {c: 3, d: 4, e: -1} custom claims beforeSignIn returns {f: 6, g: 7, e: 5} session claims beforeCreate custom claims will be completely replaced with beforeSignIn custom claims. In the Auth database, the user record will have {c: 3, d: 4, e: -1} as custom claims. In the user's ID token, the following claims will be available: {c: 3, d: 4, e: 5, f: 6, g: 7} Note that refreshing the user's token will preserve these claims: {c: 3, d: 4, e: 5, f: 6, g: 7} |
Non-200 error response | Throwing an HttpsError | Blocks user sign-up and propagates error immediately to client. No other event triggered after. |
End-to-end example
In the following example where an email/password user is created, only specific
email domains are allowed to succeed. Additional custom claims and a default
photo URL are set on the user.
Client logic
This event is triggered when a new user is created in the
client SDK.
firebase.auth().createUserWithEmailAndPassword('johndoe@example.com', 'password')
.then((result) => {
result.user.getIdTokenResult()
})
.then((idTokenResult) => {
console.log(idTokenResult.claim.admin);
})
.catch((error) => {
if (error.code === 'auth/internal-error' &&
error.message.indexOf('Cloud Function') !== -1) {
extractAndDisplayErrorMessage(error.message);
} else {
...
}
});
Server logic
A subscribed HTTP trigger will be called as a result of the above.
const gcipCloudFunctions = require('gcip-cloud-functions');
const authClient = new gcipCloudFunctions.Auth();
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
if (user.email.indexOf('@acme.com') !== -1) {
return {
customClaims: {verified: false},
photoURL: 'https://www.example.com/profile/default/photo.png',
};
}
throw new gcipCloudFunctions.https.HttpsError(
'invalid-argument', `Unauthorized email "${user.email}"`);
});
BeforeSignIn event
Event trigger
This event is triggered when an existing user attempts to sign in (credential
verified), just before the ID token and refresh tokens are returned to the
client. This event will also trigger for new users after the beforeCreate
handler is triggered and processed. However, if beforeCreate
fails,
beforeSignIn
will not trigger. If beforeSignIn
fails after beforeCreate
is
triggered, the user is still created and saved in the Auth database, but the
sign-in attempt will fail.
You can create a function that triggers before a user is signed in using the
Auth#functions().beforeSignInHandler()
event handler:
exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
});
Before a user sign-in event completes, the event handler callback will trigger
with a
UserRecord
object, identifying the user about to be signed in, and an extended
EventContext
object.
When user sign-in is allowed, the callback is expected to return a response
(synchronous or asynchronous via Promise) with the optional attributes of the
user to be modified on the user attempting to sign in. If nothing is returned,
the operation will succeed without modifying the user.
exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
return getUserAccessLevel(user.uid).then((level) => {
customClaims: {
accessLevel: level,
}
});
});
When the operation is disallowed, raise an HttpsError
. This will surface to
the client API.
exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
// Block sign-in until the user is verified.
if (!user.emailVerified)) {
throw new gcipCloudFunctions.https.HttpsError(
'invalid-argument',
`Email "${user.email}" needs to be verified before access is granted.`);
}
});
Multi-factor authentication with SMS: beforeSignIn
will only trigger on
users with second factors after the second factor challenge is successfully
solved and before the ID token is issued for that user.
Response fields that can be modified
Only modification of the following fields is allowed: displayName
, photoURL
,
customClaims
, emailVerified
, disabled
. An additional beforeSignIn
specific field, sessionClaims
, field is also supported.
Supported sign-in methods:
- Email link
- Email/Password
- Federated sign in (OAuth, OIDC, SAML)
- Phone number sign-in
- Game Center sign-in
- Multi-factor auth (SMS) sign-in
displayName | string | Persisted in database and propagated to ID token ("name" claim).. Will overwrite beforeCreate value in token if returned. |
disabled | boolean | Persisted in the database. This should block current sign-in with the USER_DISABLED error thrown client side. The disabled status of the user is persisted in the Auth database. Other live sessions will fail on token refresh with USER_DISABLED error. |
emailVerified | boolean | Persisted in database and propagated to ID token ("email_verified" claim). Will overwrite beforeCreate value in token if returned. Note that emailVerified is not propagated to the ID token if no email is set on the account. |
photoURL | string (URL) | Persisted in database and propagated to ID token ("picture" claim). Will overwrite beforeCreate value in token if returned. |
customClaims | Object with 1K byte size limit and no reserved OIDC claim names. Note that the combined sessionClaims and customClaims fields should also not exceed 1K byte. | Behaves similarly to customClaims in beforeCreate. Persisted in database and propagated to ID token but will be overwritten in user's token claims with overlapping claims defined in sessionClaims. If beforeCreate also returns customClaims, they will be replaced with beforeSignIn customClaims. Example: beforeSignIn returns {a: 1, b: 2, e: 0} custom claims beforeSignIn returns {c: 3, d: 4, e: 5} session claims In the Auth database, the user record will have {a: 1, b: 2, e: 0} as custom claims. In the user's ID token, the following claims will be available: {a: 1, b: 2, c: 3, d: 4, e: 5} Note that refreshing the user's token will preserve these claims: {a: 1, b: 2, c: 3, d: 4, e: 5} |
sessionClaims | Object with 1K byte size limit and no reserved OIDC claim names. Note that the combined sessionClaims and customClaims fields should also not exceed 1K byte. | This will only propagate to token (for current session) and will merge with beforeCreate/beforeSignIn custom claims while overwriting overlapping claims from beforeCreate/beforeSignIn. These will only be reflected in the current session and not persisted in the Auth database. This should behave like custom token claims where claims are stored in the refresh token and preserved on token refresh. If both custom and session claims are returned, the session claims will be merged with custom claims and session claims will overwrite overlapping claims. Example: customClaims defined in beforeCreate or beforeSignIn as {a: 1, b: 2, e: 0} beforeSignIn returns sessionClaims {c: 3, d: 4, e: 5} In the Auth database, the user record will have {a: 1, b: 2, e: 0} as custom claims. In the user's ID token, the following claims will be available: {a: 1, b: 2, c: 3, d: 4, e: 5} Note that refreshing the user's token will preserve these claims: {a: 1, b: 2, c: 3, d: 4, e: 5} |
Non-200 error response | | Blocks user sign-in and propagates error immediately to the client. |
End-to-end example
In the following example, an email/password user signing in will get blocked
if the request is coming from a disallowed region or IP address. In addition,
the user level of access is determined and set via custom claims before the
sign-in operation completes.
Client logic
This event is triggered when a new user is created in the
client SDK.
firebase.auth().signInWithEmailAndPassword('user@domain.com', 'password')
.then((result) => {
result.user.getIdTokenResult()
})
.then((idTokenResult) => {
console.log(idTokenResult.claim.admin);
})
.catch((error) => {
if (error.code === 'auth/internal-error' &&
error.message.indexOf('Cloud Function') !== -1) {
extractAndDisplayErrorMessage(error.message);
} else {
...
}
});
Server logic
A subscribed HTTP trigger will be called as a result of the above.
const gcipCloudFunctions = require('gcip-cloud-functions');
const authClient = new gcipCloudFunctions.Auth();
exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
if (!isSuspiciousRequest(context.ipAddress, context.userAgent)) {
return {
customClaims: {
admin: isAdmin(user)
}
};
}
throw new gcipCloudFunctions.https.HttpsError(
'permission-denied',
'Unauthorized request origin!');
});
Event Context
Whenever an event is triggered, additional event context will be provided to
the server callback. This provides additional event context, such as the
underlying resource, event type, locale, IP address, etc.
exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
console.log(context.ipAddress);
const signInProvider = context.eventType.split(':')[1];
});
A comprehensive list of the properties provided in an event context object is
documented below:
locale | Yes | The application locale. This is set via the client API (firebase.auth().languageCode = 'fr';) or by passing the locale header in the REST API. This also accepts language-region format, eg. 'sv-SE'. | fr, en, it, en-US, sv-SE, etc. |
ipAddress | No | The IP address of the end user triggering the blocking function. | 114.14.200.1 |
userAgent | No | The user agent that triggered the blocking function. | Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 |
eventId | No | The event's unique identifier. | rWsyPtolplG2TBFoOkkgyg |
eventType | No | The event type. This provides information on the event name (beforeSignIn, beforeCreate) and the associated sign-in method used, eg: google.com, password, emailLink, etc. | providers/cloud.auth/eventTypes/user.beforeSignIn:password, providers/cloud.auth/eventTypes/user.beforeCreate:google.com |
authType | No | The level of permissions for a user. For beforeSignIn and beforeCreate, there is always a known user, hence this will always be "USER". | USER |
resource | No | The resource that emitted the event. For authentication event, this is of format: projects/projectId or in a multi-tenant context: projects/projectId/tenants/tenantId | projects/project_id, or projects/project_id/tenants/tenant_id (multi-tenant context) |
timestamp | No | Timestamp for the event as an RFC 3339 string. | Tue, 23 Jul 2019 21:10:57 GMT |
additionalUserInfo | Yes | Additional user information. This is an AdditionalUserInfo object:
interface AdditionalUserInfo { // eg. saml.provider, oidc.provider, google.com, facebook.com, etc. providerId: string; // Raw user info. This is the raw user info also returned in client SDK. profile?: any; // This is the Twitter screen_name. username?: string; // Whether the user is new or existing. // This is true for beforeCreate, false for others. isNewUser: boolean; } | { providerId: 'twitter.com', profile: { friends_count: 10, profile_background_image_url: '...', id: '...', screen_name: '...', }, username: 'twitter-username', isNewUser: false } |
credential | Yes | Inbound IdP credentials if available. This is an AuthCredential object, available when signing in with federated providers, eg. social, SAML and OIDC providers. Raw OAuth credentials will only be available when enabled in the Cloud Console "Include token credentials" trigger settings and are applicable to beforeSignIn and beforeCreate events. By default, credentials are not returned.
interface AuthCredential { // All user SAML or OIDC claims. These are in plain object format but should // be verified and parsed from SAML response, IdP ID token, etc. // This is empty for all other providers. claims?: {[key: string]: any}; // Optional OAuth ID token if available and enabled in the project config. idToken?: string; // Optional OAuth access token if available and enabled in the project config. accessToken?: string; // Optional OAuth refresh token if available and enabled in the project config. refreshToken?: string; // Optional OAuth expiration if available and enabled in the project config. expirationTime?: string; // Optional OAuth token secret if available and enabled in the project config. secret?: string; // eg. saml.provider, oidc.provider, google.com, facebook.com, etc. providerId: string; }; | // Google credential. { idToken: 'GOOG_ID_TOKEN', accessToken: 'GOOG_ACCESS_TOKEN', refreshToken: 'GOOG_REFRESH_TOKEN', expirationTime: 'Tue, 08 Sep 2020 08:06:51 GMT', providerId: 'google.com' } // SAML credential. { claims: { eid: 'EMPLOYEE_ID', role: 'EMPLOYEE_ACCESS_LEVEL', groups: 'EMPLOYEE_GROUP_ID' }, providerId: 'saml.my-provider-id' } |
Blocking function error codes
The following error codes can be thrown from a blocking function via an
HttpsError
. An HttpsError
can be initialized using one of the error codes
below (following the Google Cloud API
errors).
A default error message is provided for each error code. A custom message can
also be provided to override the default message.
invalid-argument | 400 | Client specified an invalid argument. |
failed-precondition | 400 | Request can not be executed in the current system state. |
out-of-range | 400 | Client specified an invalid range. |
unauthenticated | 401 | Request not authenticated due to missing, invalid, or expired OAuth token |
permission-denied | 403 | Client does not have sufficient permission. |
not-found | 404 | Specified resource is not found. |
aborted | 409 | Concurrency conflict, such as read-modify-write conflict. |
already-exists | 409 | The resource that a client tried to create already exists. |
resource-exhausted | 429 | Either out of resource quota or reaching rate limiting. |
cancelled | 499 | Request cancelled by the client. |
data-loss | 500 | Unrecoverable data loss or data corruption. |
unknown | 500 | Unknown server error. |
internal | 500 | Internal server error. |
not-implemented | 501 | API method not implemented by the server. |
unavailable | 503 | Service unavailable. |
deadline-exceeded | 504 | Request deadline exceeded. |
Note that the HTTP non-200 status codes will not be returned to the client.
Instead they will be returned to the Auth server and wrapped in another error
object before surfacing to the client.
To throw an error with the default error message:
throw new gcipCloudFunctions.https.HttpsError('permission-denied');
To throw an error with a custom error message:
throw new gcipCloudFunctions.https.HttpsError(
'permission-denied', 'Unauthorized request origin!');
Currently, the error will be surfaced to the client side wrapping the details
of the error thrown in the blocking functions. Currently, the error is surfaced
as an INTERNAL_ERROR
, where $XYZ
is the HTTP error code (eg. 400),
$STATUS_CODE
is the error status code (eg. INVALID_ARGUMENT
) and
$BLOCKING_FUNCTION_MESSAGE
Is the default or custom error message returned
in the function.
auth/internal-error | ERROR_INTERNAL_ERROR | ERROR_INTERNAL_ERROR (FirebaseException Android Exception) | HTTP Cloud Function returned an error. Code: $XYZ, Status: "$STATUS_CODE", Message: "$BLOCKING_FUNCTION_MESSAGE" |
Example
In this example, the HttpsError
thrown in the function will surface to the
client like this:
throw new gcipCloudFunctions.https.HttpsError(
'invalid-argument', `Unauthorized email ${user.email}`);
Client SDK error code: auth/internal-error
Client error message: HTTP Cloud Function returned an error. Code: 400,
Status: "INVALID_ARGUMENT", Message: "Unauthorized email user@evil.com"
REST API backend error sample:
{
"error": {
"code": 400,
"message": "BLOCKING_FUNCTION_ERROR_RESPONSE : HTTP Cloud Function returned an error. Code: 400, Status: \"INVALID_ARGUMENT\", Message: \"Unauthorized email user@evil.com\"",
"errors": [
{
"message": "BLOCKING_FUNCTION_ERROR_RESPONSE : HTTP Cloud Function returned an error. Code: 400, Status: \"INVALID_ARGUMENT\", Message: \"Unauthorized email user@evil.com\"",
"domain": "global",
"reason": "invalid"
}
]
}
}
Forwarded OAuth credentials
The following table documents the different credentials / data that can be
forwarded (additional requirements may be needed for some of the credentials
to be returned) to the blocking functions depending on the IdP the user signs
in with:
Google | ✓ | ✓ | ✓ | | ✓ | |
Facebook | | ✓ | ✓ | | | |
Twitter | | ✓ | | ✓ | | |
GitHub | | ✓ | | | | |
Microsoft | ✓ | ✓ | ✓ | | ✓ | |
LinkedIn | | ✓ | ✓ | | | |
Yahoo | ✓ | ✓ | ✓ | | ✓ | |
Apple | ✓ | ✓ | ✓ | | ✓ | |
SAML | | | | | | ✓ |
OIDC | ✓ | ✓ | ✓ | | ✓ | ✓ |
Refresh tokens for OIDC and OAuth 2.0 based providers will be forwarded to
the blocking function if the refresh token checkbox is checked in the
Include token credentials expandable menu in the Cloud Console
Triggers section. However, some identity providers either do not expose
refresh tokens or may require additional parameters to be requested client-side
when the sign-in operation is initiated.
For all providers, when signing in directly with an OAuth credential (eg. ID
token or Access token), as opposed to the 3-legged OAuth flow: The same OAuth
credential provided client-side will be forwarded to the blocking function. No
refresh token will be available in this case.
const credential = firebase.auth.GoogleAuthProvider.credential(id_token);
firebase.auth().signInWithCredential(credential);
The documentation below explains the different behavior between providers when a 3-legged OAuth flow is used for sign-in.
Generic OIDC providers
When a user signs in with a generic OIDC provider, the following credentials
will be forwarded:
- OIDC ID token if the id_token flow is selected.
- OIDC ID token and access token if the code flow is selected.
The additional refresh token will also be made available only when the
offline_access
scope is selected.
const provider = new firebase.auth.OAuthProvider('oidc.my-provider');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);
Google
When a user signs in with Google, only the Google ID token and access tokens
are forwarded. The refresh token will only be available in the following case:
- The access_type=offline custom parameter should be requested.
- If the user previously consented and no new scope was requested, the
prompt=consent custom parameter should be requested.
Learn more about
Google refresh tokens.
const provider = new firebase.auth.GoogleAuthProvider();
provider.setCustomParameters({
'access_type': 'offline',
'prompt': 'consent'
});
firebase.auth().signInWithPopup(provider);
Facebook
Facebook identity provider does not return OAuth refresh tokens.
However, Facebook will return an access token that can be exchanged for another
access token. Learn more about the different types of
access tokens
supported by Facebook and how you can exchange them for
long-lived tokens.
GitHub
GitHub does not support refresh tokens but will return access tokens that do
not expire unless revoked. GitHub access tokens will be forwarded to the
blocking function.
Microsoft
When a user signs in with the Microsoft provider, the Microsoft ID token,
access token will be forwarded to the blocking function.
The additional refresh token will also be made available only when the
offline_access scope is selected, similar to other OIDC based providers.
const provider = new firebase.auth.OAuthProvider('microsoft.com');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);
Yahoo
When a user signs in with Yahoo, the Yahoo ID token, access token and refresh
token are always forwarded without any additional custom parameters or scopes.
LinkedIn
LinkedIn does not return refresh tokens and only an access token is provided.
Apple
"Sign in with Apple" will forward the ID token, access token and refresh token
to the blocking function.
Access Control
Modified fields returned in the function response will be propagated to the
user's ID token.
exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
return {
displayName: 'John Doe',
photoURL: 'https://lh3.googleusercontent.com/-IcZWqgdWEGY/123456789/photo.jpg',
emailVerified: true,
customClaims: {
employee_id: '987654321'
},
sessionClaims: {
role: 'admin',
group_id: '123',
},
};
});
The claims are propagated to the ID token as illustrated in the token payload
below.
{
"iss": "https://securetoken.google.com/PROJECT_ID",
"name": "John Doe",
"picture": "https://lh3.googleusercontent.com/-IcZWqgdWEGY/123456789/photo.jpg",
"aud": "PROJECT_ID",
"auth_time": 1528854045,
"user_id": "cxncXcCb84YySchlOOrFjNIOnIY2",
"sub": "cxncXcCb84YySchlOOrFjNIOnIY2",
"iat": 1528922063,
"exp": 1528925663,
"email": "johndoe@gmail.com",
"email_verified": true,
"firebase": {
"identities": {
"email": [
"johndoe@gmail.com"
],
"google.com": [
"1234567890"
]
},
"sign_in_provider": "password"
},
"role": "admin",
"group_id": "123",
"employee_id": "987654321"
}
Enforcing access based on modified token claims
Access control can also be enforced based on the modified claims returned in
the blocking functions.
Using the Admin SDK
You can manually verify the ID token of the user if you are using your own
server side code with some external database
Retrieve the ID token on the client (using web client SDK)
auth.currentUser.getIdToken().then(function(idToken) {
}).catch(function(error) {
});
Verify the ID token server side after you send it to your server
(using Node.js
Admin SDK)
admin.auth().verifyIdToken(idToken)
.then((decodedToken) => {
const uid = decodedToken.uid;
const name = decodedToken.name;
const picture = decodedToken.picture;
const emailVerified = decodedToken.email_verified;
const role = decodedToken.role;
const groupId = decodedToken.group_id;
}).catch((error) => {
});
Learn more about ID token verification from our
official documentation.
Using Firestore security rules
Access control based on persistent or session claims can be enforced via
Cloud Firestore security rules.
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read: if request.auth.uid == userId;
allow create, update, delete: if request.auth.uid == userId &&
request.auth.token.role == 'admin';
}
}
}
Sample blocking functions
The following sample blocking functions illustrate some common use cases.
Allow sign-up for certain email domains
Only allow users for a specific email domain to sign up.
export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
if (!user.email || user.email.indexOf('@acme.com') === -1) {
throw new gcipCloudFunctions.https.HttpsError(
'invalid-argument', `Unauthorized email "${user.email}"`);
}
});
Prevent sign-up for IdPs with unverified emails
Only allow sign-up via identity providers that verify emails.
export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
if (user.email && !user.emailVerified) {
throw new gcipCloudFunctions.https.HttpsError(
'invalid-argument', `Unverified email "${user.email}"`);
}
});
Block sign-in until email is verified
Allow users with unverified emails to sign up. On sign up, send an email
verification to the user. Sign in will be blocked until the user verifies
their email.
export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
const locale = context.locale;
if (user.email && !user.emailVerified) {
return admin.auth().generateEmailVerificationLink(user.email).then((link) => {
return sendCustomVerificationEmail(user.email, link, locale);
});
}
});
export.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
if (user.email && !user.emailVerified) {
throw new gcipCloudFunctions.https.HttpsError(
'invalid-argument',
`"${user.email}" needs to be verified before access is granted.`);
}
});
Inject custom claims on sign-in
On sign up, look up additional claims for the associated user from an external
database and set them as custom claims on the user.
const db = admin.firestore();
export.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
return db.collection('adminEmails').doc(user.email).get()
.then((doc) => {
customClaims: {admin: doc.exists}
});
});
Treat Facebook sign-up emails as verified
If a developer considers certain identity providers as verified, set the email
as verified on sign up.
export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
if (user.email &&
!user.emailVerified &&
context.eventType.indexOf(':facebook.com') !== -1) {
return {
emailVerified: true,
};
}
});
Block sign-in from certain IP addresses
Block sign in from certain regions or IP addresses.
export.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
if (isSuspiciousIpAddress(context.ipAddress)) {
throw new gcipCloudFunctions.https.HttpsError(
'permission-denied', 'Unauthorized access!');
}
});
Track sign-in IP address source for ID token theft detection
The IP address could be injected into the ID token claims on sign-in.
This is useful to detect possible token theft. For example if a sign-in event
was detected in one region and then an authenticated request with an ID token
from the same session is sent from a geographically different region,
re-authentication could be required for that user.
export.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
return {
sessionClaims: {
signInIpAddress: context.ipAddress,
},
};
});
On authenticated access, the sign-in IP address source can be compared relative
to request IP address source.
app.post('/getRestrictedData', (req, res) => {
const idToken = req.body.idToken;
admin.auth().verifyIdToken(idToken, true).then((claims) => {
const requestIpAddress = req.connection.remoteAddress;
const signInIpAddress = claims.signInIpAddress;
if (!isSuspiciousIpAddressChange(signInIpAddress, requestIpAddress)) {
res.status(401).send({error: 'Unauthorized access. Please login again!'});
} else {
getData(claims).then(data => {
res.end(JSON.stringify(data);
}, error => {
res.status(500).send({ error: 'Server error!' })
});
}
});
});
Add unique session ID for each sign-in
Give each sign-in attempt its own unique session identifier. This unlocks
session tracking and session management capabilities.
export.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
return {
sessionClaims: {
sessionId: createUniqueSessionIdentifier(user),
},
};
});
Screen user photos before creation
Screen user display names or photos for inappropriate content using machine
learning APIs.
export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
if (user.photoURL) {
return isPhotoAppropriate(user.photoURL)
.then((status) => {
if (!status) {
return {
photoURL: PLACEHOLDER_GUEST_PHOTO_URL,
};
}
});
});
Set custom and session claims with IdP claims
Set SAML IdP claims on user's custom and session claims.
export.beforeSignIn = authClient.functions().beforeCreateHandler((user, context) => {
if (context.credential &&
context.credential.providerId === 'saml.my-provider-id') {
return {
customClaims: {
eid: context.credential.claims.employeeid,
},
sessionClaims: {
role: context.credential.claims.role,
groups: context.credential.claims.groups,
}
}
}
});
Access IdP OAuth credentials
Get Google user's refresh token and call Google APIs.
In this example, the refresh token is stored for offline access and a Google
Calendar event is scheduled.
const {OAuth2Client} = require('google-auth-library');
const {google} = require('googleapis');
const keys = require('./oauth2.keys.json');
const oAuth2Client = new OAuth2Client(
keys.web.client_id,
keys.web.client_secret
);
export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
if (context.credential &&
context.credential.providerId === 'google.com') {
return saveUserRefreshToken(
user.uid,
context.credential.refreshToken,
'google.com'
)
.then(() => {
return new Promise((resolve, reject) => {
oAuth2Client.setCredentials({
access_token: context.credential.accessToken,
refresh_token: context.credential.refreshToken,
});
const calendar = google.calendar('v3');
const event = {};
calendar.events.insert({
auth: oAuth2Client,
calendarId: 'primary',
resource: event,
}, (err, event) => {
resolve();
});
});
})
}
});