Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
171 changes: 167 additions & 4 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 {AuthRequestHandler} 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 {deepCopy, deepExtend} from '../utils/deep-copy';


/**
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 {
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 {AbstractAuthRequestHandler} 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: AuthRequestHandler,
protected readonly authRequestHandler: AbstractAuthRequestHandler,
cryptoSigner: CryptoSigner) {
this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner);
this.sessionCookieVerifier = createSessionCookieVerifier(projectId);
Expand Down Expand Up @@ -599,11 +603,153 @@ class BaseAuth {
}


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

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

Choose a reason for hiding this comment

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

Just mark the constructor argument as public readonly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One problem with this is that if they are not using typescript, they can overwrite this without any error thrown. I tried it and the property can be overwritten. Since this is publicly available, i think we should enforce the readonly.

Copy link
Contributor

Choose a reason for hiding this comment

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

I expected the TS compiler to generate a readonly getter for this, but looks like that only happens if we expose this as a getter in the code. So this is ok.

Copy link
Contributor

Choose a reason for hiding this comment

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

I expected the TS compiler to add a readonly getter, but looks like that only happens if we define a getter explicitly here. So this is ok.

}

/**
* 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;
});
}

/**
* Imports the list of users provided to Firebase Auth. This is useful when
* migrating from an external authentication system without having to use the Firebase CLI SDK.
* At most, 1000 users are allowed to be imported one at a time.
* When importing a list of password users, UserImportOptions are required to be specified.
*
* @param {UserImportRecord[]} users The list of user records to import to Firebase Auth.
* @param {UserImportOptions=} options The user import options, required when the users provided
* include password credentials.
* @return {Promise<UserImportResult>} A promise that resolves when the operation completes
* with the result of the import. This includes the number of successful imports, the number
* of failed uploads and their corresponding errors.
*/
public importUsers(
users: UserImportRecord[], options?: UserImportOptions): Promise<UserImportResult> {
// Verify users have matching tenantIds if provided.
// When undefined tenant ID is provided (implicitly set the tenant ID of the TenantAwareAuth),
// the Auth server will automatically set the tenant ID.
users.forEach((user: UserImportRecord, index: number) => {
if (validator.isNonEmptyString(user.tenantId) &&
user.tenantId !== this.tenantId) {
throw new FirebaseAuthError(AuthClientErrorCode.MISMATCHING_TENANT_ID);
}
});
return super.importUsers(users, options);
}
}


/**
* Auth service bound to the provided app.
* An Auth instance can have multiple tenants.
*/
export class Auth extends BaseAuth implements FirebaseServiceInterface {
public INTERNAL: AuthInternals = new AuthInternals();
private tenantsMap: {[key: string]: TenantAwareAuth};
Copy link
Contributor

Choose a reason for hiding this comment

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

readonly

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

private readonly app_: FirebaseApp;

/**
Expand Down Expand Up @@ -632,6 +778,7 @@ export class Auth extends BaseAuth implements FirebaseServiceInterface {
new AuthRequestHandler(app),
cryptoSignerFromApp(app));
this.app_ = app;
this.tenantsMap = {};
}

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

/**
* Returns a TenantAwareAuth instance for the corresponding tenant ID.
*
* @param {string} tenantId The tenant ID whose TenantAwareAuth is to be returned.
* @return {TenantAwareAuth} The corresponding TenantAwareAuth instance.
*/
public forTenant(tenantId: string): TenantAwareAuth {
if (!validator.isNonEmptyString(tenantId)) {
throw new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID);
}
if (typeof this.tenantsMap[tenantId] === 'undefined') {
this.tenantsMap[tenantId] = new TenantAwareAuth(this, tenantId);
}
return this.tenantsMap[tenantId];
}
}
6 changes: 6 additions & 0 deletions src/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,10 @@ export class AuthClientErrorCode {
message: 'The domain of the continue URL is not whitelisted. Whitelist the domain in the ' +
'Firebase console.',
};
public static UNSUPPORTED_TENANT_OPERATION = {
code: 'unsupported-tenant-operation',
message: 'This operation is not supported in a multi-tenant context.',
};
public static USER_NOT_FOUND = {
code: 'user-not-found',
message: 'There is no user record corresponding to the provided identifier.',
Expand Down Expand Up @@ -827,6 +831,8 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = {
TOKEN_EXPIRED: 'ID_TOKEN_EXPIRED',
// Continue URL provided in ActionCodeSettings has a domain that is not whitelisted.
UNAUTHORIZED_DOMAIN: 'UNAUTHORIZED_DOMAIN',
// Operation is not supported in a multi-tenant context.
UNSUPPORTED_TENANT_OPERATION: 'UNSUPPORTED_TENANT_OPERATION',
// User on which action is to be performed is not found.
USER_NOT_FOUND: 'USER_NOT_FOUND',
// Password provided is too weak.
Expand Down
Loading