To use external identities with Identity-Aware Proxy (IAP), your app needs a sign-in page. IAP will redirect users to this page to authenticate before they can access secure resources.
This article shows you how to build an authentication page from scratch. Constructing this page yourself is complex, but gives you full control over the authentication flow and user experience.
If you don't need to fully customize your UI, consider letting IAP host a sign-in page for you or using FirebaseUI instead to simplify your code.
Before you begin
Enable external identities, and select the I'll provide my own UI option during setup.
Installing the gcip-iap library
The gcip-iap
NPM module abstracts the communications between
your application, IAP, and Identity Platform.
Using the library is strongly recommended. It lets you customize the entire authentication flow without worrying about the underlying exchanges between the UI and IAP.
Include the library as a dependency like this:
// Import Firebase/GCIP dependencies. These are installed on npm install.
import * as firebase from 'firebase/app';
import 'firebase/auth';
// Import GCIP/IAP module.
import * as ciap from 'gcip-iap';
The previous instructions are for gcip-iap
v0.1.4 or less.
Starting with version v1.0.0, gcip-iap
requires the firebase
v9 peer
dependency or greater.
If you are migrating to gcip-iap
v1.0.0 or above, complete the following
actions:
- Update the
firebase
version in yourpackage.json
file to v9.6.0+. - Update the
firebase
import statements as follows.
No additional code changes are needed:
// Import firebase modules.
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
// Import gcip-iap module.
import * as ciap from 'gcip-iap';
Implementing AuthenticationHandler
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;
}
To customize the UI, you need to create a custom class that implements the interface. There are no restrictions on what JavaScript framework you use. The sections below provide additional information on how to build each method.
Selecting a tenant
If you're using multi-tenancy, you need to select a tenant before you can authenticate a user. If you only have a single tenant, or are using project-level authentication, you can skip this step.
IAP supports the same providers as Identity Platform, such as:
- Email and password
- OAuth (Google, Facebook, Twitter, GitHub, Microsoft, etc)
- SAML
- OIDC
- Phone number
- Custom
- Anonymous
Note that phone number, custom, and anonymous authentication are not supported for multi-tenancy.
To select a tenant, the library invokes the selectTenant()
callback. You
can implement this method to choose a tenant programmatically, or
display a UI so the user can select one themselves.
To pick a tenant programmatically, leverage the current context. The
Authentication
class contains a getOriginalURL()
method that returns the
URL the user was accessing before needing to authenticate. You can use this to
locate a match from a list of associated tenants.
// Select provider 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);
});
}
If you choose to display a UI, there are a variety of approaches for determining what provider to use. For example, you could display a list of tenants and have the user choose one, or you could ask them to enter their email address and then locate a match based on the domain.
// Select provider by showing UI.
selectTenant(projectConfig, tenantIds) {
return new Promise((resolve, reject) => {
// Show UI to select the tenant.
renderSelectTenant(
tenantIds,
// On tenant selection.
(selectedTenantId) => {
resolve({
tenantId: selectedTenantId,
// If associated provider IDs can also be determined,
// populate this list.
providerIds: [],
// If email is available, populate this field too.
email: undefined,
});
});
});
}
Regardless of your approach, the returned SelectedTenantInfo
object will be
used later to complete the authentication flow. It contains the ID of the
selected tenant, any provider IDs, and the email the user entered.
Getting the Auth object
Once you have a provider, you need a way of obtaining an Auth
object.
Implement the getAuth()
callback to return a
firebase.auth.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()
is used to track where the user corresponding to the
provided configuration is stored. It also makes it possible to silently
refresh a previously authenticated user's Identity Platform ID token
without requiring the user to re-enter their credentials.
If you're using multiple IAP resources with different
tenants, we recommended using a unique Auth
instance for each. This allows
multiple resources with different configurations to use the same authentication
page. It also allows multiple users to sign in at the same time without logging
out the previous user.
The following is an example of how to implement getAuth()
:
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;
}
Signing users in
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.
In a multi-tenant environment, the available authentication methods can be
determined from SelectedTenantInfo
, if it was provided. This variable contains
the same information returned by the selectTenant()
callback.
The following example shows how you might implement startSignIn()
for
an existing user with an email and password:
startSignIn(auth, selectedTenantInfo) {
return new Promise((resolve, reject) => {
// Show UI to sign-in or sign-up a user.
$('#sign-in-form').on('submit', (e) => {
const email = $('#email').val();
const password = $('#password').val();
// Example: ask user for email and password.
// The method of sign in may have already been determined from the
// selectedTenantInfo object.
auth.signInWithEmailAndPassword(email, password)
.then((userCredential) => {
resolve(userCredential);
})
.catch((error) => {
// Show error message.
});
});
});
}
You can also sign in users with a federated provider, such as SAML or OIDC, using a popup or redirect:
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 firebase.auth.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);
});
}
Some OAuth providers support passing a login hint for sign-in:
startSignIn(auth, selectedTenantInfo) {
// Show UI to sign-in or sign-up a user.
return new Promise((resolve, reject) => {
// Use selectedTenantInfo to determine the provider and pass the login hint
// if that provider supports it and the user specified an email.
if (selectedTenantInfo &&
selectedTenantInfo.providerIds &&
selectedTenantInfo.providerIds.indexOf('microsoft.com') !== -1) {
const provider = new firebase.auth.OAuthProvider('microsoft.com');
provider.setCustomParameters({
login_hint: selectedTenantInfo.email || undefined,
});
} else {
// Figure out the provider used...
}
auth.signInWithPopup(provider)
.then((userCredential) => {
resolve(userCredential);
})
.catch((error) => {
// Show error message.
});
});
}
See the Identity Platform documentation to learn more about authenticating users.
Processing a user
The optional processUser()
method allows you to modify a signed in user
before redirecting back to the IAP resource. You might use
this to:
- Link to additional providers.
- Update the user's profile.
- Ask the user for additional data after registration.
- Process OAuth access tokens returned by
getRedirectResult()
after callingsignInWithRedirect()
.
The following is an example of implementing processUser()
:
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;
});
}
Note that if you want any changes to a user reflected in the ID token claims propagated by IAP to your app, you'll need to force the token to refresh:
processUser(user) {
return user.updateProfile({
photoURL: 'https://example.com/profile/1234/photo.png',
}).then(function() {
// To reflect updated photoURL in the ID token, force token
// refresh.
return user.getIdToken(true);
}).then(function() {
return user;
});
}
Displaying a progress UI
Implement the optional showProgressBar()
and hideProgressBar()
callbacks
to display a custom progress UI to the user whenever the gcip-iap
module
is executing long-running network tasks.
Handling errors
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).
The following example shows how you might implement handleError()
:
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;
});
}
The table below lists error codes specific to IAP that can
be thrown. Identity Platform can also throw errors; see the documentation for
firebase.auth.Auth
.
Error code | Description |
---|---|
invalid-argument |
Client specified an invalid argument. |
failed-precondition |
Request can not be executed in the current system state. |
out-of-range |
Client specified an invalid range. |
unauthenticated |
Request not authenticated due to a missing, invalid, or expired OAuth token. |
permission-denied |
Client does not have sufficient permission, or the UI is hosted on an unauthorized domain. |
not-found . |
Specified resource is not found. |
aborted |
Concurrency conflict, such as read-modify-write conflict. |
already-exists |
The resource that a client tried to create already exists. |
resource-exhausted |
Either out of resource quota or reaching rate limiting. |
cancelled |
Request cancelled by the client. |
data-loss |
Unrecoverable data loss or data corruption. |
unknown |
Unknown server error. |
internal |
Internal server error. |
not-implemented |
API method not implemented by the server. |
unavailable |
Service unavailable. |
restart-process |
Revisit the URL that redirected you to this page to restart the authentication process. |
deadline-exceeded |
Request deadline exceeded. |
authentication-uri-fail |
Failed to generate authentication URI. |
gcip-token-invalid |
Invalid GCIP ID token provided. |
gcip-redirect-invalid |
Invalid redirect URL. |
get-project-mapping-fail |
Failed to get project ID. |
gcip-id-token-encryption-error |
GCIP ID token encryption error. |
gcip-id-token-decryption-error |
GCIP ID token decryption error. |
gcip-id-token-unescape-error |
Web safe base64 unescape failed. |
resource-missing-gcip-sign-in-url |
Missing GCIP authentication URL for the specified IAP resource. |
Signing users out
In some cases, you may want to allow users to sign out from all current sessions that share the same authentication URL.
After a user signs out, there may be no URL to redirect them back to
(this commonly occurs when a user signs out from all tenants associated with a
sign-in page). In this case, implement the completeSignOut()
callback to
display a message indicating the user logged out successfully. If you don't,
a blank page will appear.
Using your custom UI
Once you've created a class that implements AuthenticationHandler
, you can
use it to create a new Authentication
instance, and start it.
// Implement interface AuthenticationHandler.
// const authHandlerImplementation = ....
const ciapInstance = new ciap.Authentication(authHandlerImplementation);
ciapInstance.start();
Deploy your application and navigate to the authentication page. You should see your custom sign-in UI.
What's next
- Learn how to access non-Google resources programmatically.
- Learn about managing sessions.
- Gain a deeper understanding of how external identities work with IAP.