
Security News
MCP Community Begins Work on Official MCP Metaregistry
The MCP community is launching an official registry to standardize AI tool discovery and let agents dynamically find and install MCP servers.
gcip-cloud-functions
Advanced tools
Google Cloud Identity Platform Blocking Functions for Node.js
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
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
displayName
, photoURL
, disabled
,
emailVerified
, etc).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:
beforeSignIn
events.Deploy an HTTP trigger for the blocking function
Using the Cloud Console
Using gcloud CLI Install gcloud SDK, if you have not already done so.
Initialize gcloud via command line:
gcloud init
# Update components to latest.
gcloud components update
Follow the instructions below on how to write an authentication blocking function.
# Deploy a beforeCreate HTTP trigger.
gcloud functions deploy $before_create_func_name --runtime nodejs10 --trigger-http --allow-unauthenticated
# Deploy a beforeSignIn HTTP trigger.
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:
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.
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
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.
# Deploy a beforeCreate HTTP trigger.
gcloud functions deploy $before_create_func_name --runtime nodejs10 \
--trigger-http --allow-unauthenticated --set-env-vars GCP_PROJECT=$project_id
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 {
// If no display name is provided, set it to Guest.
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.
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:
Supported response fields | Type | beforeCreate behavior |
---|---|---|
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. |
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.
// Blocking functions can also be triggered in a multi-tenant context before user creation.
// firebase.auth().tenantId = 'tenant-id-1';
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) {
// Unauthorized email "johndoe@example.com".
extractAndDisplayErrorMessage(error.message);
} else {
...
}
});
Server logic
A subscribed HTTP trigger will be called as a result of the above.
// Import the Cloud Auth Admin module.
const gcipCloudFunctions = require('gcip-cloud-functions');
// Initialize the Auth client.
const authClient = new gcipCloudFunctions.Auth();
// Http trigger with Cloud Functions.
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
// If the user is authenticating within a tenant context,
// the tenant ID can be determined from
// user.tenantId or from context.resource,
// eg. 'projects/project-id/tenant/tenant-id-1'
// Only users of a specific domain can sign up.
if (user.email.indexOf('@acme.com') !== -1) {
// Add custom claim and photo URL to authorized user before resolving.
return {
// User still needs to be verified by admin.
customClaims: {verified: false},
// Automatically add some default guest photo URL.
photoURL: 'https://www.example.com/profile/default/photo.png',
};
}
// Errors will translate to code auth/internal-error on the client side.
throw new gcipCloudFunctions.https.HttpsError(
'invalid-argument', `Unauthorized email "${user.email}"`);
});
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:
// Http trigger with Cloud Functions.
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) => {
// Check current user's access level and return it in user's custom claims.
// This will be persisted in the database for all sessions. For session only
// persistence, sessionClaims should be used instead.
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.
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:
Supported response fields | Type | beforeSignIn behavior |
---|---|---|
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. |
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.
// Blocking functions can also be triggered in a multi-tenant context
// before user sign-in.
// firebase.auth().tenantId = 'tenant-id-1';
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) {
// Unauthorized request origin!
extractAndDisplayErrorMessage(error.message);
} else {
...
}
});
Server logic
A subscribed HTTP trigger will be called as a result of the above.
// Import the Cloud Auth Admin module.
const gcipCloudFunctions = require('gcip-cloud-functions');
// Initialize the Auth client.
const authClient = new gcipCloudFunctions.Auth();
// Http trigger with Cloud Functions.
exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
// If the user is authenticating within a tenant context,
// the tenant ID can be determined from user.tenantId or from context.resource,
// eg. 'projects/project-id/tenant/tenant-id-1'
if (!isSuspiciousRequest(context.ipAddress, context.userAgent)) {
return {
customClaims: {
// Check if this existing user is an admin.
admin: isAdmin(user)
}
};
}
throw new gcipCloudFunctions.https.HttpsError(
'permission-denied',
'Unauthorized request origin!');
});
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);
// Get type of provider used to sign in with:
const signInProvider = context.eventType.split(':')[1]; // facebook.com
});
A comprehensive list of the properties provided in an event context object is documented below:
Property | Optional | Description | Example |
---|---|---|---|
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' } |
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.
Error code | HTTP Status code | Default error 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.
Web error codes (error.code) | iOS Error name | Android Error Code | Description (error.message in JS SDK) |
---|---|---|---|
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"
}
]
}
}
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:
IdP | ID token | Access Token | Expiration Time | Token Secret | Refresh Token | Sign in claims |
---|---|---|---|---|---|---|
✓ | ✓ | ✓ | ✓ | |||
✓ | ✓ | |||||
✓ | ✓ | |||||
GitHub | ✓ | |||||
Microsoft | ✓ | ✓ | ✓ | ✓ | ||
✓ | ✓ | |||||
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.
// The same credential provided here will be forwarded to the blocking function.
// In this case, the Google ID token will be forwarded.
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.
When a user signs in with a generic OIDC provider, the following credentials will be forwarded:
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);
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:
Learn more about Google refresh tokens.
const provider = new firebase.auth.GoogleAuthProvider();
provider.setCustomParameters({
'access_type': 'offline',
'prompt': 'consent'
});
firebase.auth().signInWithPopup(provider);
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 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.
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);
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 does not return refresh tokens and only an access token is provided.
"Sign in with Apple" will forward the ID token, access token and refresh token to the blocking function.
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",
// Profile changes are also persisted in the database.
"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",
// Profile changes are also persisted in the database.
"email_verified": true,
"firebase": {
"identities": {
"email": [
"johndoe@gmail.com"
],
"google.com": [
"1234567890"
]
},
"sign_in_provider": "password"
},
// sessionClaims apply to the current session only.
"role": "admin",
"group_id": "123",
// customClaims are persisted on all sessions.
"employee_id": "987654321"
}
Access control can also be enforced based on the modified claims returned in the blocking functions.
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) {
// Send token to your backend via HTTPS
// ...
}).catch(function(error) {
// Handle error
});
Verify the ID token server side after you send it to your server (using Node.js Admin SDK)
// idToken comes from the client app.
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) => {
// Handle error
});
Learn more about ID token verification from our official documentation.
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';
}
}
}
The following sample blocking functions illustrate some common use cases.
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}"`);
}
});
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}"`);
}
});
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) {
// Send custom email verification on sign-up
// https://firebase.google.com/docs/auth/admin/email-action-links
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.`);
}
});
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) => {
// Set admin to true if the user is an admin.
customClaims: {admin: doc.exists}
});
});
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 regions or IP addresses.
export.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
if (isSuspiciousIpAddress(context.ipAddress)) {
throw new gcipCloudFunctions.https.HttpsError(
'permission-denied', 'Unauthorized access!');
}
});
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) => {
// Get the ID token passed.
const idToken = req.body.idToken;
// Verify the ID token, check if revoked and decode its payload.
admin.auth().verifyIdToken(idToken, true).then((claims) => {
// Get request IP address
const requestIpAddress = req.connection.remoteAddress;
// Get sign-in IP address.
const signInIpAddress = claims.signInIpAddress;
// Check if the request IP address origin is suspicious relative to session
// IP address.
// The current request timestamp and the auth_time of the ID
// token can provide additional signals of abuse especially if the IP
// address suddenly changed. If there was a sudden geographical change in a
// short period of time, then it will give stronger signals of possible
// abuse.
if (!isSuspiciousIpAddressChange(signInIpAddress, requestIpAddress)) {
// Suspicious IP address change. Require re-authentication.
// You can also revoke all user sessions by calling:
// admin.auth().revokeRefreshTokens(claims.sub).
res.status(401).send({error: 'Unauthorized access. Please login again!'});
} else {
// Access is valid. Try to return data.
getData(claims).then(data => {
res.end(JSON.stringify(data);
}, error => {
res.status(500).send({ error: 'Server error!' })
});
}
});
});
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 display names or photos for inappropriate content using machine learning APIs.
export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
if (user.photoURL) {
// https://cloud.google.com/blog/products/gcp/filtering-inappropriate-content-with-the-cloud-vision-api
return isPhotoAppropriate(user.photoURL)
.then((status) => {
if (!status) {
// Sanitize inappropriate photos by replacing them with guest photos.
// Users could also be blocked from sign-up, disabled, etc.
return {
photoURL: PLACEHOLDER_GUEST_PHOTO_URL,
};
}
});
});
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 {
// Employee ID does not change so save in persistent claims (stored in
// Auth DB).
customClaims: {
eid: context.credential.claims.employeeid,
},
// Copy role and groups to token claims. These will not be persisted.
sessionClaims: {
role: context.credential.claims.role,
groups: context.credential.claims.groups,
}
}
}
});
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');
// ...
// Initialize Google OAuth client.
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') {
// Store the refresh token for later offline use.
// These will only be returned if refresh tokens credentials are included
// (enabled by Cloud Console).
return saveUserRefreshToken(
user.uid,
context.credential.refreshToken,
'google.com'
)
.then(() => {
// Blocking the function is not required. The function can resolve while
// this operation continues to run in the background.
return new Promise((resolve, reject) => {
// For this operation to succeed, the appropriate OAuth scope should
// be requested on sign in with Google, client-side. In this case:
// https://www.googleapis.com/auth/calendar
// You can check granted_scopes from within:
// context.additionalUserInfo.profile.granted_scopes (space joined
// list of scopes).
// Set access token/refresh token.
oAuth2Client.setCredentials({
access_token: context.credential.accessToken,
refresh_token: context.credential.refreshToken,
});
const calendar = google.calendar('v3');
// Setup Onboarding event on user's calendar.
const event = {/** ... */};
calendar.events.insert({
auth: oAuth2Client,
calendarId: 'primary',
resource: event,
}, (err, event) => {
// Do not fail. This is a best effort approach.
resolve();
});
});
})
}
});
FAQs
Google Cloud Identity Platform Blocking Functions for Node.js
The npm package gcip-cloud-functions receives a total of 7,581 weekly downloads. As such, gcip-cloud-functions popularity was classified as popular.
We found that gcip-cloud-functions demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
The MCP community is launching an official registry to standardize AI tool discovery and let agents dynamically find and install MCP servers.
Research
Security News
Socket uncovers an npm Trojan stealing crypto wallets and BullX credentials via obfuscated code and Telegram exfiltration.
Research
Security News
Malicious npm packages posing as developer tools target macOS Cursor IDE users, stealing credentials and modifying files to gain persistent backdoor access.