gcip-cloud-functions
TypeScript icon, indicating that this package has built-in type declarations

0.2.0 • Public • Published

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

  1. Before you start
  2. Blocking functions general behavior
  3. Writing blocking functions
  4. Forwarded OAuth credentials
  5. Access Control
  6. Sample blocking functions

Before you start

To use blocking functions (beforeCreate and beforeSignIn) with Google Cloud Identity Platform:

  1. Create a project on Google Cloud Console and enable Identity Platform.

  2. 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.
  3. 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
      # 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.

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

# 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

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 {
    // 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.

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

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.

// 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}"`);
});

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:

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

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

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.

// 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!');
});

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);
  // 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'
}

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.

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"
      }
    ]
  }
}

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:

IdP ID token Access Token Expiration Time Token Secret Refresh Token Sign in claims
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.

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

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",
  // 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"
}

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) {
  // 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.

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) {
    // 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.`);
  }
});

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) => {
      // Set admin to true if the user is an admin.
      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) => {
  // 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!' })
      });
    }
  });
});

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) {
    // 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 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 {
      // 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,
      }
    }
  }
});

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');
// ...
// 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();
          });
      });
    })
 }
});

Package Sidebar

Install

npm i gcip-cloud-functions

Weekly Downloads

3,411

Version

0.2.0

License

Apache-2.0

Unpacked Size

312 kB

Total Files

40

Last publish

Collaborators

  • google-wombot