Skip to content

feat(auth): Multi-tenancy support for Google Cloud Identity Platform #628

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Sep 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f94423f
Starts defining multi-tenancy APIs. This includes: (#526)
bojeil-google May 23, 2019
5c7d44d
Multi-tenancy changes to shared Auth API requests (#539)
bojeil-google Jun 3, 2019
8507d93
Defines TenantAwareAuth (#551)
bojeil-google Jun 10, 2019
5a36d39
Defines tenant management API on AuthRequestHandler. (#559)
bojeil-google Jun 12, 2019
5aa4dc0
Adds tenant management APIs to developer facing Auth instance. (#567)
bojeil-google Jun 14, 2019
835e08f
Merged with master
hiranya911 Jun 14, 2019
99eef4a
Adds integration tests for tenant management APIs. (#570)
bojeil-google Jun 18, 2019
6158b1f
Defines Auth multi-tenancy references in index.d.ts. (#572)
bojeil-google Jun 21, 2019
3f37111
Adds basic integration tests for the following in multi-tenancy conte…
bojeil-google Jun 27, 2019
f5d8163
Removes all usage of tenant types. (#611)
bojeil-google Aug 7, 2019
904e118
Defines the TenantManager class and its underlying methods. (#617)
bojeil-google Aug 8, 2019
1124348
Removes tenant management methods and `forTenant` from Auth class. (#…
bojeil-google Aug 13, 2019
83aea9b
Updates multi-tenancy integration tests. (#623)
bojeil-google Aug 20, 2019
664cf1f
Merge branch 'master' into temp-multi-tenancy
bojeil-google Aug 20, 2019
c437e0a
Merge branch 'master' into temp-multi-tenancy
bojeil-google Aug 21, 2019
e67d4eb
Addresses review comments.
bojeil-google Aug 21, 2019
da7d073
Merge branch 'master' into temp-multi-tenancy
bojeil-google Sep 4, 2019
9110c8a
Sync to head.
bojeil-google Sep 4, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 96 additions & 37 deletions package-lock.json

Large diffs are not rendered by default.

422 changes: 391 additions & 31 deletions src/auth/auth-api-request.ts

Large diffs are not rendered by default.

137 changes: 123 additions & 14 deletions src/auth/auth-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,115 @@ export interface OIDCAuthProviderRequest extends OIDCUpdateAuthProviderRequest {
/** The public API request interface for updating a generic Auth provider. */
export type UpdateAuthProviderRequest = SAMLUpdateAuthProviderRequest | OIDCUpdateAuthProviderRequest;

/** The email provider configuration interface. */
export interface EmailSignInProviderConfig {
enabled?: boolean;
passwordRequired?: boolean; // In the backend API, default is true if not provided
}

/** The server side email configuration request interface. */
export interface EmailSignInConfigServerRequest {
allowPasswordSignup?: boolean;
enableEmailLinkSignin?: boolean;
}


/**
* Defines the email sign-in config class used to convert client side EmailSignInConfig
* to a format that is understood by the Auth server.
*/
export class EmailSignInConfig implements EmailSignInProviderConfig {
public readonly enabled?: boolean;
public readonly passwordRequired?: boolean;

/**
* Static method to convert a client side request to a EmailSignInConfigServerRequest.
* Throws an error if validation fails.
*
* @param {any} options The options object to convert to a server request.
* @return {EmailSignInConfigServerRequest} The resulting server request.
*/
public static buildServerRequest(options: EmailSignInProviderConfig): EmailSignInConfigServerRequest {
const request: EmailSignInConfigServerRequest = {};
EmailSignInConfig.validate(options);
if (options.hasOwnProperty('enabled')) {
request.allowPasswordSignup = options.enabled;
}
if (options.hasOwnProperty('passwordRequired')) {
request.enableEmailLinkSignin = !options.passwordRequired;
}
return request;
}

/**
* Validates the EmailSignInConfig options object. Throws an error on failure.
*
* @param {any} options The options object to validate.
*/
private static validate(options: {[key: string]: any}) {
// TODO: Validate the request.
const validKeys = {
enabled: true,
passwordRequired: true,
};
if (!validator.isNonNullObject(options)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'"EmailSignInConfig" must be a non-null object.',
);
}
// Check for unsupported top level attributes.
for (const key in options) {
if (!(key in validKeys)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
`"${key}" is not a valid EmailSignInConfig parameter.`,
);
}
}
// Validate content.
if (typeof options.enabled !== 'undefined' &&
!validator.isBoolean(options.enabled)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'"EmailSignInConfig.enabled" must be a boolean.',
);
}
if (typeof options.passwordRequired !== 'undefined' &&
!validator.isBoolean(options.passwordRequired)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'"EmailSignInConfig.passwordRequired" must be a boolean.',
);
}
}

/**
* The EmailSignInConfig constructor.
*
* @param {any} response The server side response used to initialize the
* EmailSignInConfig object.
* @constructor
*/
constructor(response: {[key: string]: any}) {
if (typeof response.allowPasswordSignup === 'undefined') {
throw new FirebaseAuthError(
AuthClientErrorCode.INTERNAL_ERROR,
'INTERNAL ASSERT FAILED: Invalid email sign-in configuration response');
}
this.enabled = response.allowPasswordSignup;
this.passwordRequired = !response.enableEmailLinkSignin;
}

/** @return {object} The plain object representation of the email sign-in config. */
public toJSON(): object {
return {
enabled: this.enabled,
passwordRequired: this.passwordRequired,
};
}
}


/**
* Defines the SAMLConfig class used to convert a client side configuration to its
Expand Down Expand Up @@ -367,24 +476,24 @@ export class SAMLConfig implements SAMLAuthProviderConfig {
AuthClientErrorCode.INTERNAL_ERROR,
'INTERNAL ASSERT FAILED: Invalid SAML configuration response');
}
utils.addReadonlyGetter(this, 'providerId', SAMLConfig.getProviderIdFromResourceName(response.name));
this.providerId = SAMLConfig.getProviderIdFromResourceName(response.name);
// RP config.
utils.addReadonlyGetter(this, 'rpEntityId', response.spConfig.spEntityId);
utils.addReadonlyGetter(this, 'callbackURL', response.spConfig.callbackUri);
this.rpEntityId = response.spConfig.spEntityId;
this.callbackURL = response.spConfig.callbackUri;
// IdP config.
utils.addReadonlyGetter(this, 'idpEntityId', response.idpConfig.idpEntityId);
utils.addReadonlyGetter(this, 'ssoURL', response.idpConfig.ssoUrl);
utils.addReadonlyGetter(this, 'enableRequestSigning', !!response.idpConfig.signRequest);
this.idpEntityId = response.idpConfig.idpEntityId;
this.ssoURL = response.idpConfig.ssoUrl;
this.enableRequestSigning = !!response.idpConfig.signRequest;
const x509Certificates: string[] = [];
for (const cert of (response.idpConfig.idpCertificates || [])) {
if (cert.x509Certificate) {
x509Certificates.push(cert.x509Certificate);
}
}
utils.addReadonlyGetter(this, 'x509Certificates', x509Certificates);
this.x509Certificates = x509Certificates;
// When enabled is undefined, it takes its default value of false.
utils.addReadonlyGetter(this, 'enabled', !!response.enabled);
utils.addReadonlyGetter(this, 'displayName', response.displayName);
this.enabled = !!response.enabled;
this.displayName = response.displayName;
}

/** @return {SAMLAuthProviderConfig} The plain object representation of the SAMLConfig. */
Expand Down Expand Up @@ -555,12 +664,12 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
AuthClientErrorCode.INTERNAL_ERROR,
'INTERNAL ASSERT FAILED: Invalid OIDC configuration response');
}
utils.addReadonlyGetter(this, 'providerId', OIDCConfig.getProviderIdFromResourceName(response.name));
utils.addReadonlyGetter(this, 'clientId', response.clientId);
utils.addReadonlyGetter(this, 'issuer', response.issuer);
this.providerId = OIDCConfig.getProviderIdFromResourceName(response.name);
this.clientId = response.clientId;
this.issuer = response.issuer;
// When enabled is undefined, it takes its default value of false.
utils.addReadonlyGetter(this, 'enabled', !!response.enabled);
utils.addReadonlyGetter(this, 'displayName', response.displayName);
this.enabled = !!response.enabled;
this.displayName = response.displayName;
}

/** @return {OIDCAuthProviderConfig} The plain object representation of the OIDCConfig. */
Expand Down
137 changes: 131 additions & 6 deletions src/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
import {UserRecord, CreateRequest, UpdateRequest} from './user-record';
import {FirebaseApp} from '../firebase-app';
import {FirebaseTokenGenerator, CryptoSigner, cryptoSignerFromApp} from './token-generator';
import {FirebaseAuthRequestHandler} from './auth-api-request';
import {
AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler,
} from './auth-api-request';
import {AuthClientErrorCode, FirebaseAuthError, ErrorInfo} from '../utils/error';
import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service';
import {
Expand All @@ -32,6 +34,7 @@ import {
AuthProviderConfig, AuthProviderConfigFilter, ListProviderConfigResults, UpdateAuthProviderRequest,
SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse,
} from './auth-config';
import {TenantManager} from './tenant-manager';


/**
Expand Down Expand Up @@ -72,6 +75,7 @@ export interface DecodedIdToken {
iat: number;
iss: string;
sub: string;
tenant?: string;
[key: string]: any;
}

Expand All @@ -85,7 +89,7 @@ export interface SessionCookieOptions {
/**
* Base Auth class. Mainly used for user management APIs.
*/
class BaseAuth {
export class BaseAuth<T extends AbstractAuthRequestHandler> {
protected readonly tokenGenerator: FirebaseTokenGenerator;
protected readonly idTokenVerifier: FirebaseTokenVerifier;
protected readonly sessionCookieVerifier: FirebaseTokenVerifier;
Expand All @@ -94,14 +98,14 @@ class BaseAuth {
* The BaseAuth class constructor.
*
* @param {string} projectId The corresponding project ID.
* @param {FirebaseAuthRequestHandler} authRequestHandler The RPC request handler
* @param {T} authRequestHandler The RPC request handler
* for this instance.
* @param {CryptoSigner} cryptoSigner The instance crypto signer used for custom token
* minting.
* @constructor
*/
constructor(protected readonly projectId: string,
protected readonly authRequestHandler: FirebaseAuthRequestHandler,
protected readonly authRequestHandler: T,
cryptoSigner: CryptoSigner) {
this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner);
this.sessionCookieVerifier = createSessionCookieVerifier(projectId);
Expand Down Expand Up @@ -599,11 +603,126 @@ class BaseAuth {
}


/**
* The tenant aware Auth class.
*/
export class TenantAwareAuth extends BaseAuth<TenantAwareAuthRequestHandler> {
public readonly tenantId: string;

/**
* The TenantAwareAuth class constructor.
*
* @param {object} app The app that created this tenant.
* @param tenantId The corresponding tenant ID.
* @constructor
*/
constructor(private readonly app: FirebaseApp, tenantId: string) {
super(
utils.getProjectId(app),
new TenantAwareAuthRequestHandler(app, tenantId),
cryptoSignerFromApp(app));
utils.addReadonlyGetter(this, 'tenantId', tenantId);
}

/**
* Creates a new custom token that can be sent back to a client to use with
* signInWithCustomToken().
*
* @param {string} uid The uid to use as the JWT subject.
* @param {object=} developerClaims Optional additional claims to include in the JWT payload.
*
* @return {Promise<string>} A JWT for the provided payload.
*/
public createCustomToken(uid: string, developerClaims?: object): Promise<string> {
// This is not yet supported by the Auth server. It is also not yet determined how this will be
// supported.
return Promise.reject(
new FirebaseAuthError(AuthClientErrorCode.UNSUPPORTED_TENANT_OPERATION));
}

/**
* Verifies a JWT auth token. Returns a Promise with the tokens claims. Rejects
* the promise if the token could not be verified. If checkRevoked is set to true,
* verifies if the session corresponding to the ID token was revoked. If the corresponding
* user's session was invalidated, an auth/id-token-revoked error is thrown. If not specified
* the check is not applied.
*
* @param {string} idToken The JWT to verify.
* @param {boolean=} checkRevoked Whether to check if the ID token is revoked.
* @return {Promise<DecodedIdToken>} A Promise that will be fulfilled after a successful
* verification.
*/
public verifyIdToken(idToken: string, checkRevoked: boolean = false): Promise<DecodedIdToken> {
return super.verifyIdToken(idToken, checkRevoked)
.then((decodedClaims) => {
// Validate tenant ID.
if (decodedClaims.firebase.tenant !== this.tenantId) {
throw new FirebaseAuthError(AuthClientErrorCode.MISMATCHING_TENANT_ID);
}
return decodedClaims;
});
}

/**
* Creates a new Firebase session cookie with the specified options that can be used for
* session management (set as a server side session cookie with custom cookie policy).
* The session cookie JWT will have the same payload claims as the provided ID token.
*
* @param {string} idToken The Firebase ID token to exchange for a session cookie.
* @param {SessionCookieOptions} sessionCookieOptions The session cookie options which includes
* custom session duration.
*
* @return {Promise<string>} A promise that resolves on success with the created session cookie.
*/
public createSessionCookie(
idToken: string, sessionCookieOptions: SessionCookieOptions): Promise<string> {
// Validate arguments before processing.
if (!validator.isNonEmptyString(idToken)) {
return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN));
}
if (!validator.isNonNullObject(sessionCookieOptions) ||
!validator.isNumber(sessionCookieOptions.expiresIn)) {
return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION));
}
// This will verify the ID token and then match the tenant ID before creating the session cookie.
return this.verifyIdToken(idToken)
.then((decodedIdTokenClaims) => {
return super.createSessionCookie(idToken, sessionCookieOptions);
});
}

/**
* Verifies a Firebase session cookie. Returns a Promise with the tokens claims. Rejects
* the promise if the token could not be verified. If checkRevoked is set to true,
* verifies if the session corresponding to the session cookie was revoked. If the corresponding
* user's session was invalidated, an auth/session-cookie-revoked error is thrown. If not
* specified the check is not performed.
*
* @param {string} sessionCookie The session cookie to verify.
* @param {boolean=} checkRevoked Whether to check if the session cookie is revoked.
* @return {Promise<DecodedIdToken>} A Promise that will be fulfilled after a successful
* verification.
*/
public verifySessionCookie(
sessionCookie: string, checkRevoked: boolean = false): Promise<DecodedIdToken> {
return super.verifySessionCookie(sessionCookie, checkRevoked)
.then((decodedClaims) => {
if (decodedClaims.firebase.tenant !== this.tenantId) {
throw new FirebaseAuthError(AuthClientErrorCode.MISMATCHING_TENANT_ID);
}
return decodedClaims;
});
}
}


/**
* Auth service bound to the provided app.
* An Auth instance can have multiple tenants.
*/
export class Auth extends BaseAuth implements FirebaseServiceInterface {
export class Auth extends BaseAuth<AuthRequestHandler> implements FirebaseServiceInterface {
public INTERNAL: AuthInternals = new AuthInternals();
private readonly tenantManager_: TenantManager;
private readonly app_: FirebaseApp;

/**
Expand All @@ -629,9 +748,10 @@ export class Auth extends BaseAuth implements FirebaseServiceInterface {
constructor(app: FirebaseApp) {
super(
Auth.getProjectId(app),
new FirebaseAuthRequestHandler(app),
new AuthRequestHandler(app),
cryptoSignerFromApp(app));
this.app_ = app;
this.tenantManager_ = new TenantManager(app);
}

/**
Expand All @@ -642,4 +762,9 @@ export class Auth extends BaseAuth implements FirebaseServiceInterface {
get app(): FirebaseApp {
return this.app_;
}

/** @return The current Auth instance's tenant manager. */
public tenantManager(): TenantManager {
return this.tenantManager_;
}
}
Loading