Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
198 changes: 198 additions & 0 deletions src/auth/tenant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*!
* Copyright 2019 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as utils from '../utils';
import * as validator from '../utils/validator';
import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error';
import {
EmailSignInConfig, EmailSignInConfigServerRequest, EmailSignInProviderConfig,
} from './auth-config';

/** The server side tenant type enum. */
export type TenantServerType = 'LIGHTWEIGHT' | 'FULL_SERVICE' | 'TYPE_UNSPECIFIED';

/** The client side tenant type enum. */
export type TenantType = 'lightweight' | 'full_service' | 'type_unspecified';

/** The TenantOptions interface used for create/read/update tenant operations. */
export interface TenantOptions {
displayName?: string;
type?: TenantType;
emailSignInConfig?: EmailSignInProviderConfig;
}

/** The corresponding server side representation of a TenantOptions object. */
export interface TenantOptionsServerRequest extends EmailSignInConfigServerRequest {
displayName?: string;
type?: TenantServerType;
}

/** The tenant server response interface. */
export interface TenantServerResponse {
name: string;
type?: TenantServerType;
displayName?: string;
allowPasswordSignup: boolean;
enableEmailLinkSignin: boolean;
}

/** The interface representing the listTenant API response. */
export interface ListTenantsResult {
tenants: Tenant[];
pageToken?: string;
}


/**
* Tenant class that defines a Firebase Auth tenant.
*/
export class Tenant {
public readonly tenantId: string;
public readonly type?: TenantType;
public readonly displayName?: string;
public readonly emailSignInConfig?: EmailSignInConfig;

/**
* Builds the corresponding server request for a TenantOptions object.
*
* @param {TenantOptions} tenantOptions The properties to convert to a server request.
* @param {boolean} createRequest Whether this is a create request.
* @return {object} The equivalent server request.
*/
public static buildServerRequest(
tenantOptions: TenantOptions, createRequest: boolean): TenantOptionsServerRequest {
Tenant.validate(tenantOptions, createRequest);
let request: TenantOptionsServerRequest = {};
if (typeof tenantOptions.emailSignInConfig !== 'undefined') {
request = EmailSignInConfig.buildServerRequest(tenantOptions.emailSignInConfig);
}
if (typeof tenantOptions.displayName !== 'undefined') {
request.displayName = tenantOptions.displayName;
}
if (typeof tenantOptions.type !== 'undefined') {
request.type = tenantOptions.type.toUpperCase() as TenantServerType;
}
return request;
}

/**
* Returns the tenant ID corresponding to the resource name if available.
*
* @param {string} resourceName The server side resource name
* @return {?string} The tenant ID corresponding to the resource, null otherwise.
*/
public static getTenantIdFromResourceName(resourceName: string): string | null {
// name is of form projects/project1/tenants/tenant1
const matchTenantRes = resourceName.match(/\/tenants\/(.*)$/);
if (!matchTenantRes || matchTenantRes.length < 2) {
return null;
}
return matchTenantRes[1];
}

/**
* Validates a tenant options object. Throws an error on failure.
*
* @param {any} request The tenant options object to validate.
* @param {boolean} createRequest Whether this is a create request.
*/
private static validate(request: any, createRequest: boolean) {
const validKeys = {
displayName: true,
type: true,
emailSignInConfig: true,
};
const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest';
if (!validator.isNonNullObject(request)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
`"${label}" must be a valid non-null object.`,
);
}
// Check for unsupported top level attributes.
for (const key in request) {
if (!(key in validKeys)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
`"${key}" is not a valid ${label} parameter.`,
);
}
}
// Validate displayName type if provided.
if (typeof request.displayName !== 'undefined' &&
!validator.isNonEmptyString(request.displayName)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
`"${label}.displayName" must be a valid non-empty string.`,
);
}
// Validate type if provided.
if (typeof request.type !== 'undefined' && !createRequest) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'"Tenant.type" is an immutable property.',
);
}
if (createRequest &&
request.type !== 'full_service' &&
request.type !== 'lightweight') {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
`"${label}.type" must be either "full_service" or "lightweight".`,
);
}
// Validate emailSignInConfig type if provided.
if (typeof request.emailSignInConfig !== 'undefined') {
// This will throw an error if invalid.
EmailSignInConfig.buildServerRequest(request.emailSignInConfig);
}
}

/**
* The Tenant object constructor.
*
* @param {any} response The server side response used to initialize the Tenant object.
* @constructor
*/
constructor(response: any) {
const tenantId = Tenant.getTenantIdFromResourceName(response.name);
if (!tenantId) {
throw new FirebaseAuthError(
AuthClientErrorCode.INTERNAL_ERROR,
'INTERNAL ASSERT FAILED: Invalid tenant response',
);
}
this.tenantId = tenantId;
this.displayName = response.displayName;
this.type = (response.type && response.type.toLowerCase()) || undefined;
try {
this.emailSignInConfig = new EmailSignInConfig(response);
} catch (e) {
this.emailSignInConfig = undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to put a comment here to indicate why it's ok to ignore the error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}
}

/** @return {object} The plain object representation of the tenant. */
public toJSON(): object {
return {
tenantId: this.tenantId,
displayName: this.displayName,
type: this.type,
emailSignInConfig: this.emailSignInConfig && this.emailSignInConfig.toJSON(),
};
}
}

3 changes: 3 additions & 0 deletions src/auth/user-import-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface UserImportRecord {
customClaims?: object;
passwordHash?: Buffer;
passwordSalt?: Buffer;
tenantId?: string;
}


Expand All @@ -87,6 +88,7 @@ interface UploadAccountUser {
lastLoginAt?: number;
createdAt?: number;
customAttributes?: string;
tenantId?: string;
}


Expand Down Expand Up @@ -153,6 +155,7 @@ function populateUploadAccountUser(
photoUrl: user.photoURL,
phoneNumber: user.phoneNumber,
providerUserInfo: [],
tenantId: user.tenantId,
customAttributes: user.customClaims && JSON.stringify(user.customClaims),
};
if (typeof user.passwordHash !== 'undefined') {
Expand Down
Loading