Skip to content
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
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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