Cloud Identity-Aware Proxy (Cloud IAP) controls access to your cloud applications and VMs running on Google Cloud Platform (GCP). Cloud IAP verifies user identity and the context of the request to determine if a user should be allowed to access an application or a VM.
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.
By default, IAP uses Google identities and Cloud IAM. External identities can be used with IAP by leveraging Identity Platform. This allows the end users to do authentication using any identity and identity provider that GCIP supports. This means that developers will be able to use IdPs that are supported by GCIP (SAML, OIDC, social etc) to authenticate users.
To use external identities with IAP, your app needs a page for authenticating users. IAP will redirect any unauthenticated requests to this page.
gcip-iap
NPM module implements the protocol used to integrate
GCIP (Google Cloud's Identity Platform) for authenticating external identities
with IAP (Identity Aware Proxy).
iap-gcip-web-toolkit
repository provides quick-start samples demonstrating how
to integrate a GCIP authentication UI with a GAE application
gated with Cloud IAP.
Sample authentication UI pages are provided using:
In order to use external identities with IAP, an authentication page needs to be hosted to handle authentication, tenant selection, token refreshes, sign-out and all authentication related operations. This page can be hosted anywhere and the same page can be shared for multiple IAP resources or GCP projects.
The gcip-iap
NPM module is provided to abstract the underlying communication
between GCIP and IAP on that authentication page. This will expose callbacks
for UI and authentication related logic.
You will be able to build your own authentication UI on top of this via
these callbacks using the GCIP JS library. You can also use the pre-built
FirebaseUI
library as your authentication UI.
This will allow you to use all GCIP supported external providers such as:
Learn more on how to [create an authentication UI with FirebaseUI](### Create an Authentication UI with FirebaseUI) or [create your own custom authentication UI](### Create your own Custom Authentication UI).
FirebaseUI, an open-source JavaScript library provides simple, customizable elements that reduce boilerplate code when building the authentication UI. It includes flows a wide range of identity providers, including username/password, OAuth, SAML, OIDC, and more.
FirebaseUI makes it easy to build an authentication page for using external
identities with IAP by providing an implementation of the
AuthenticationHandler
required by the gcip-iap
module.
FirebaseUI can be configured for the following use cases:
The UI provides the following additional capabilities:
With FirebaseUI
, a configuration object needs to be provided in order to render
authentication UI as required. Additional customizations can be done via CSS
overrides or via provided callbacks.
Starting with version v1.0.0, gcip-iap
requires the firebase
V9 peer dependency or greater.
No additional changes are needed beyond updating the import mechanism for firebase
V9:
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
// The rest of the code is the same.
// Import GCIP/IAP module.
import * as ciap from 'gcip-iap';
See the Firebase upgrade guide for more information.
For integrations with FirebaseUI, firebaseui
V6 or greater would be required.
firebaseui
import remains the same:
import * as firebaseui from 'firebaseui';
The following snippet demonstrates using a shared authentication page for
2 project configurations keyed by their respective API keys. Snippet below is
using gcip-iap v1.0.0 requiring firebase
V9 and firebaseui
V6.
// Import GCIP/Firebase and FirebaseUI dependencies.
// These are installed with npm install.
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
import * as firebaseui from 'firebaseui';
// Import GCIP/IAP module.
import * as ciap from 'gcip-iap';
// The project configuration.
const configs = {
// Configuration for project identified by API key API_KEY1.
API_KEY1: {
authDomain: 'project-id1.firebaseapp.com',
// Decide whether to ask user for identifier to figure out
// what tenant to select or whether to present all the tenants to select from.
displayMode: 'optionFirst', // Or identifierFirst
// The terms of service URL and privacy policy URL for the page
// where the user select tenant or enter email for tenant/provider
// matching.
tosUrl: 'http://localhost/tos',
privacyPolicyUrl: 'http://localhost/privacypolicy',
callbacks: {
// The callback to trigger when the selection tenant page
// or enter email for tenant matching page is shown.
selectTenantUiShown: () => {
// Show title and additional display info.
},
// The callback to trigger when the sign-in page
// is shown.
signInUiShown: (tenantId) => {
// Show tenant title and additional display info.
},
beforeSignInSuccess: (user) => {
// Do additional processing on user before sign-in is
// complete.
return Promise.resolve(user);
}
},
tenants: {
// Tenant configuration for tenant ID tenantId1.
tenantId1: {
// Display name, button color and icon URL of the
// tenant selection button. Only needed if you are
// using the option first option.
displayName: 'ACME',
buttonColor: '#2F2F2F',
iconUrl: '<icon-url-of-sign-in-button>',
// Sign-in providers enabled for tenantId1.
signInOptions: [
// Microsoft sign-in.
{
provider: 'google.com',
providerName: 'Microsoft',
buttonColor: '#2F2F2F',
iconUrl: '<icon-url-of-sign-in-button>',
loginHintKey: 'login_hint'
},
// Email/password sign-in.
{
provider: EmailAuthProvider.PROVIDER_ID,
// Do not require display name on sign up.
requireDisplayName: false
},
// SAML provider. (multiple SAML providers can be passed)
{
provider: 'saml.my-provider1',
providerName: 'SAML provider',
buttonColor: '#4666FF',
iconUrl: 'https://www.example.com/photos/my_idp/saml.png'
},
],
// If there is only one sign-in provider eligible for the user,
// whether to show the provider selection page.
immediateFederatedRedirect: true,
signInFlow: 'redirect', // Or popup
// The terms of service URL and privacy policy URL for the sign-in page
// specific to each tenant.
tosUrl: 'http://localhost/tenant1/tos',
privacyPolicyUrl: 'http://localhost/tenant1/privacypolicy'
},
// Tenant configuration for tenant ID tenantId2.
tenantId2: {
displayName: 'OCP',
buttonColor: '#2F2F2F',
iconUrl: '<icon-url-of-sign-in-button>',
// Tenant2 supports a SAML, OIDC and Email/password sign-in.
signInOptions: [
// Email/password sign-in.
{
provider: EmailAuthProvider.PROVIDER_ID,
// Do not require display name on sign up.
requireDisplayName: false
},
// SAML provider. (multiple SAML providers can be passed)
{
provider: 'saml.my-provider2',
providerName: 'SAML provider',
buttonColor: '#4666FF',
iconUrl: 'https://www.example.com/photos/my_idp/saml.png'
},
// OIDC provider. (multiple OIDC providers can be passed)
{
provider: 'oidc.my-provider1',
providerName: 'OIDC provider',
buttonColor: '#4666FF',
iconUrl: 'https://www.example.com/photos/my_idp/oidc.png'
},
],
},
},
},
// Configuration for project identified by API key API_KEY2.
// This is useful in case the same URL is used for multiple projects.
API_KEY2: {
authDomain: 'project-id2.firebaseapp.com',
displayMode: 'optionFirst',
tosUrl: 'http://localhost/tos',
privacyPolicyUrl: 'http://localhost/privacypolicy',
callbacks: {
// The callback to trigger when the selection tenant page
// or enter email for tenant matching page is shown.
selectTenantUiShown: () => {
// Show title and additional display info.
},
// The callback to trigger when the sign-in page
// is shown.
signInUiShown: (tenantId) => {
// Show tenant title and additional display info.
},
beforeSignInSuccess: (user) => {
// Do additional processing on user before sign-in is
// complete.
return Promise.resolve(user);
}
},
tenants: {
// For project level IdPs, _ is used as an identifier.
_: {
// ...
},
}
},
};
// This will handle the underlying handshake for sign-in, sign-out,
// token refreshes, safe redirect to callback URL, etc.
const handler = new firebaseui.auth.FirebaseUiHandler(
'#firebaseui-auth-container', configs);
const ciapInstance = new ciap.Authentication(handler);
ciapInstance.start();
For a more comprehensive documentation, refer to the
FirebaseUI
documentation.
To use external identities with IAP, your app needs a page that handles authentication-related operations such as tenant selection, user authentication, token refreshes, sign-out, etc.
The gcip-iap
library defines an interface AuthenticationHandler
that can be
implemented to handle UI customizations for the above operations. If you don't
need a fully customizable UI, consider
[using FirebaseUI](### Create an Authentication UI with FirebaseUI) instead to
simplify your code.
The gcip-iap
NPM module abstracts the communications between your application,
IAP, and Identity Platform.
Using the library is strongly recommended. It allows you to customize the entire authentication flow without worrying about the underlying exchanges between the UI and IAP.
Include the library as a dependency like this, if you are using gcip-iap v0.1.4 or less:
// Import Firebase/GCIP dependencies. These are installed on npm install.
import firebase from 'firebase/app';
import 'firebase/auth';
// Import GCIP/IAP module.
import * as ciap from 'gcip-iap';
If you are migrating to gcip-iap v1.0.0, refer to above [create your own custom authentication UI](### Create your own Custom Authentication UI) for more details.
The gcip-iap
module defines an interface named AuthenticationHandler
.
The library automatically calls its methods at the appropriate time to handle
authentication. The interface looks like this:
interface AuthenticationHandler {
languageCode?: string | null;
getAuth(apiKey: string, tenantId: string | null): FirebaseAuth;
startSignIn(auth: FirebaseAuth, match?: SelectedTenantInfo): Promise<UserCredential>;
selectTenant?(projectConfig: ProjectConfig, tenantIds: string[]): Promise<SelectedTenantInfo>;
completeSignOut(): Promise<void>;
processUser?(user: User): Promise<User>;
showProgressBar?(): void;
hideProgressBar?(): void;
handleError?(error: Error | CIAPError): void;
}
When building your own custom authentication UI, you will need to implement
the AuthenticationHandler
interface, and pass an instance of that to
Authentication
constructor:
import * as ciap from 'gcip-iap';
// Implement interface AuthenticationHandler.
// const authHandlerImplementation = ....
const ciapInstance = new ciap.Authentication(authHandlerImplementation);
ciapInstance.start();
The sections below provide additional information on how to build each method.
The first step to authenticating a user is determining a tenant.
To choose a tenant, the library invokes the selectTenant()
callback.
You can opt to select tenant programmatically, or display a UI so the user
can select one themselves.
This method is not required when no tenant or only a single tenant is associated with the IAP resource.
This example demonstrates how to pick the best matching tenant programmatically by checking the original URL the user was visiting.
// Select tenant programmatically.
selectTenant(projectConfig, tenantIds) {
return new Promise((resolve, reject) => {
// Show UI to select the tenant.
auth.getOriginalURL()
.then((originalUrl) => {
resolve({
tenantId: getMatchingTenantBasedOnVisitedUrl(originalUrl),
// If associated provider IDs can also be determined,
// populate this list.
providerIds: [],
});
})
.catch(reject);
});
}
Once you have a tenant, you need a way of obtaining an Auth
object.
Implement the getAuth()
callback to return an
Auth
instance corresponding to the API key and tenant ID provided.
If no tenant ID is provided, it should use project-level identity providers
instead.
getAuth(apiKey, tenantId) {
let auth = null;
// Make sure the expected API key is being used.
if (apiKey !== expectedApiKey) {
throw new Error('Invalid project!');
}
try {
auth = firebase.app(tenantId || undefined).auth();
// Tenant ID should be already set on initialization below.
} catch (e) {
// Use different App names for every tenant. This makes it possible to have
// multiple users signed in at the same time (one per tenant).
const app = firebase.initializeApp(this.config, tenantId || '[DEFAULT]');
auth = app.auth();
// Set the tenant ID on the Auth instance.
auth.tenantId = tenantId || null;
}
return auth;
}
To handle sign in, implement the startSignIn()
callback. It should display
a UI for the user to authenticate, and then return a
UserCredential
for the signed in user on completion.
This example demonstrates how to sign in a user with a SAML provider using a popup or a redirect via the GCIP web SDK.
startSignIn(auth, selectedTenantInfo) {
// Show UI to sign-in or sign-up a user.
return new Promise((resolve, reject) => {
// Provide user multiple buttons to sign-in.
// For example sign-in with popup using a SAML provider.
// The method of sign in may have already been determined from the
// selectedTenantInfo object.
const provider = new SAMLAuthProvider('saml.myProvider');
auth.signInWithPopup(provider)
.then((userCredential) => {
resolve(userCredential);
})
.catch((error) => {
// Show error message.
});
// Using redirect flow. When the page redirects back and sign-in completes,
// ciap will detect the result and complete sign-in without any additional
// action.
auth.signInWithRedirect(provider);
});
}
In some cases, you may want to allow users to sign out from all current
sessions that share the same authentication URL.
In some case where a user signs out from all tenants associated with a
sign-in page, the completeSignOut()
callback needs to be implemented
to display a message indicating the user logged out successfully.
Otherwise, a blank page will appear at the end of the flow.
For a more comprehensive documentation of the API, refer to the official Google Cloud documentation on creating a custom authentication UI for IAP external identities.
The optional processUser()
method allows you to modify a signed in user
before redirecting back to the IAP resource. This might be used to link to
additional providers, update the user's profile, ask user for additiona data
after registration, etc.
In the following example, the user was previously signed in using
signInWithRedirect
. On redirect back to the authentication page after
completing sign in with the IdP, getRedirectResult()
may be called
to retrieve additional IdP profile information or OAuth tokens
before completing sign-in and redirecting back to IAP.
processUser(user) {
return lastAuthUsed.getRedirectResult().then(function(result) {
// Save additional data, or ask user for additional profile information
// to store in database, etc.
if (result) {
// Save result.additionalUserInfo.
// Save result.credential.accessToken for OAuth provider, etc.
}
// Return the user.
return user;
});
}
Implement the optional showProgressBar()
and hideProgressBar()
callbacks
to display a custom progress UI to the user whenever the gcip-iap
module is
making long-running network tasks.
handleError()
is an optional callback for error handling. Implement it to
display error messages to users, or attempt to recover from certain errors
(such as network timeout).
In the following example, a retry button may be shown to the end user to recover from a network error when the device goes offline while processing the authentication operation.
handleError(error) {
showAlert({
code: error.code,
message: error.message,
// Whether to show the retry button. This is only available if the error is
// recoverable via retrial.
retry: !!error.retry,
});
// When user clicks retry, call error.retry();
$('.alert-link').on('click', (e) => {
error.retry();
e.preventDefault();
return false;
});
}
class Authentication {
constructor(handler: AuthenticationHandler);
start(): void;
getOriginalURL(): Promise<string | null>;
}
interface SelectedTenantInfo {
email?: string;
tenantId: string | null;
providerIds?: string[];
}
interface ProjectConfig {
projectId: string;
apiKey: string;
}
interface AuthenticationHandler {
languageCode?: string | null;
getAuth(apiKey: string, tenantId: string | null): FirebaseAuth;
startSignIn(auth: FirebaseAuth, match?: SelectedTenantInfo): Promise<UserCredential>;
selectTenant?(projectConfig: ProjectConfig, tenantIds: string[]): Promise<SelectedTenantInfo>;
completeSignOut(): Promise<void>;
processUser?(user: User): Promise<User>;
showProgressBar?(): void;
hideProgressBar?(): void;
handleError?(error: Error | CIAPError): void;
}
Refer to the sample folder for a more in-depth example, showcasing how a GAE app can be gated by IAP with GCIP authenticated users. The sample app can be used with any authentication page, whether it is custom built or built with FirebaseUI.
The sample includes:
Multiple sample codes for building and deploying authentication pages for common use cases and in various JavaScript frameworks are provided: