Skip to content

Commit 855a901

Browse files
committed
Refactor CryptoSigner (#21)
* Refactor Crypto Signer * Introduce new CryptoSignerError type * reorder imports * PR fixes * PR clean up
1 parent be4ebc6 commit 855a901

File tree

7 files changed

+590
-393
lines changed

7 files changed

+590
-393
lines changed

src/auth/auth.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier,
2222
} from './identifier';
2323
import { FirebaseApp } from '../firebase-app';
24-
import { FirebaseTokenGenerator, EmulatedSigner, cryptoSignerFromApp } from './token-generator';
24+
import { FirebaseTokenGenerator, EmulatedSigner, handleCryptoSignerError } from './token-generator';
2525
import {
2626
AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler, useEmulator,
2727
} from './auth-api-request';
@@ -36,6 +36,7 @@ import {
3636
SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse,
3737
} from './auth-config';
3838
import { TenantManager } from './tenant-manager';
39+
import { cryptoSignerFromApp } from '../utils/crypto-signer';
3940

4041
import UserIdentifier = auth.UserIdentifier;
4142
import CreateRequest = auth.CreateRequest;
@@ -82,8 +83,7 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> implements BaseAuthI
8283
if (tokenGenerator) {
8384
this.tokenGenerator = tokenGenerator;
8485
} else {
85-
const cryptoSigner = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app);
86-
this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner);
86+
this.tokenGenerator = createFirebaseTokenGenerator(app);
8787
}
8888

8989
this.sessionCookieVerifier = createSessionCookieVerifier(app);
@@ -772,9 +772,8 @@ export class TenantAwareAuth
772772
* @constructor
773773
*/
774774
constructor(app: FirebaseApp, tenantId: string) {
775-
const cryptoSigner = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app);
776-
const tokenGenerator = new FirebaseTokenGenerator(cryptoSigner, tenantId);
777-
super(app, new TenantAwareAuthRequestHandler(app, tenantId), tokenGenerator);
775+
super(app, new TenantAwareAuthRequestHandler(app, tenantId),
776+
createFirebaseTokenGenerator(app, tenantId));
778777
utils.addReadonlyGetter(this, 'tenantId', tenantId);
779778
}
780779

@@ -887,3 +886,13 @@ export class Auth extends BaseAuth<AuthRequestHandler> implements AuthInterface
887886
return this.tenantManager_;
888887
}
889888
}
889+
890+
function createFirebaseTokenGenerator(app: FirebaseApp,
891+
tenantId?: string): FirebaseTokenGenerator {
892+
try {
893+
const signer = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app);
894+
return new FirebaseTokenGenerator(signer, tenantId);
895+
} catch (err) {
896+
throw handleCryptoSignerError(err);
897+
}
898+
}

src/auth/token-generator.ts

Lines changed: 48 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,16 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { FirebaseApp } from '../firebase-app';
19-
import { ServiceAccountCredential } from '../credential/credential-internal';
20-
import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error';
21-
import { AuthorizedHttpClient, HttpError, HttpRequestConfig, HttpClient } from '../utils/api-request';
18+
import {
19+
AuthClientErrorCode, ErrorInfo, FirebaseAuthError
20+
} from '../utils/error';
21+
import { CryptoSigner, CryptoSignerError, CryptoSignerErrorCode } from '../utils/crypto-signer';
2222

2323
import * as validator from '../utils/validator';
2424
import { toWebSafeBase64 } from '../utils';
2525
import { Algorithm } from 'jsonwebtoken';
26+
import { HttpError } from '../utils/api-request';
2627

27-
28-
const ALGORITHM_RS256: Algorithm = 'RS256' as const;
2928
const ALGORITHM_NONE: Algorithm = 'none' as const;
3029

3130
const ONE_HOUR_IN_SECONDS = 60 * 60;
@@ -39,32 +38,6 @@ export const BLACKLISTED_CLAIMS = [
3938
// Audience to use for Firebase Auth Custom tokens
4039
const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit';
4140

42-
/**
43-
* CryptoSigner interface represents an object that can be used to sign JWTs.
44-
*/
45-
export interface CryptoSigner {
46-
47-
/**
48-
* The name of the signing algorithm.
49-
*/
50-
readonly algorithm: Algorithm;
51-
52-
/**
53-
* Cryptographically signs a buffer of data.
54-
*
55-
* @param {Buffer} buffer The data to be signed.
56-
* @return {Promise<Buffer>} A promise that resolves with the raw bytes of a signature.
57-
*/
58-
sign(buffer: Buffer): Promise<Buffer>;
59-
60-
/**
61-
* Returns the ID of the service account used to sign tokens.
62-
*
63-
* @return {Promise<string>} A promise that resolves with a service account ID.
64-
*/
65-
getAccountId(): Promise<string>;
66-
}
67-
6841
/**
6942
* Represents the header of a JWT.
7043
*/
@@ -87,148 +60,6 @@ interface JWTBody {
8760
tenant_id?: string;
8861
}
8962

90-
/**
91-
* A CryptoSigner implementation that uses an explicitly specified service account private key to
92-
* sign data. Performs all operations locally, and does not make any RPC calls.
93-
*/
94-
export class ServiceAccountSigner implements CryptoSigner {
95-
96-
algorithm = ALGORITHM_RS256;
97-
98-
/**
99-
* Creates a new CryptoSigner instance from the given service account credential.
100-
*
101-
* @param {ServiceAccountCredential} credential A service account credential.
102-
*/
103-
constructor(private readonly credential: ServiceAccountCredential) {
104-
if (!credential) {
105-
throw new FirebaseAuthError(
106-
AuthClientErrorCode.INVALID_CREDENTIAL,
107-
'INTERNAL ASSERT: Must provide a service account credential to initialize ServiceAccountSigner.',
108-
);
109-
}
110-
}
111-
112-
/**
113-
* @inheritDoc
114-
*/
115-
public sign(buffer: Buffer): Promise<Buffer> {
116-
const crypto = require('crypto'); // eslint-disable-line @typescript-eslint/no-var-requires
117-
const sign = crypto.createSign('RSA-SHA256');
118-
sign.update(buffer);
119-
return Promise.resolve(sign.sign(this.credential.privateKey));
120-
}
121-
122-
/**
123-
* @inheritDoc
124-
*/
125-
public getAccountId(): Promise<string> {
126-
return Promise.resolve(this.credential.clientEmail);
127-
}
128-
}
129-
130-
/**
131-
* A CryptoSigner implementation that uses the remote IAM service to sign data. If initialized without
132-
* a service account ID, attempts to discover a service account ID by consulting the local Metadata
133-
* service. This will succeed in managed environments like Google Cloud Functions and App Engine.
134-
*
135-
* @see https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signBlob
136-
* @see https://cloud.google.com/compute/docs/storing-retrieving-metadata
137-
*/
138-
export class IAMSigner implements CryptoSigner {
139-
algorithm = ALGORITHM_RS256;
140-
141-
private readonly httpClient: AuthorizedHttpClient;
142-
private serviceAccountId?: string;
143-
144-
constructor(httpClient: AuthorizedHttpClient, serviceAccountId?: string) {
145-
if (!httpClient) {
146-
throw new FirebaseAuthError(
147-
AuthClientErrorCode.INVALID_ARGUMENT,
148-
'INTERNAL ASSERT: Must provide a HTTP client to initialize IAMSigner.',
149-
);
150-
}
151-
if (typeof serviceAccountId !== 'undefined' && !validator.isNonEmptyString(serviceAccountId)) {
152-
throw new FirebaseAuthError(
153-
AuthClientErrorCode.INVALID_ARGUMENT,
154-
'INTERNAL ASSERT: Service account ID must be undefined or a non-empty string.',
155-
);
156-
}
157-
this.httpClient = httpClient;
158-
this.serviceAccountId = serviceAccountId;
159-
}
160-
161-
/**
162-
* @inheritDoc
163-
*/
164-
public sign(buffer: Buffer): Promise<Buffer> {
165-
return this.getAccountId().then((serviceAccount) => {
166-
const request: HttpRequestConfig = {
167-
method: 'POST',
168-
url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:signBlob`,
169-
data: { payload: buffer.toString('base64') },
170-
};
171-
return this.httpClient.send(request);
172-
}).then((response: any) => {
173-
// Response from IAM is base64 encoded. Decode it into a buffer and return.
174-
return Buffer.from(response.data.signedBlob, 'base64');
175-
}).catch((err) => {
176-
if (err instanceof HttpError) {
177-
const error = err.response.data;
178-
if (validator.isNonNullObject(error) && error.error) {
179-
const errorCode = error.error.status;
180-
const description = 'Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens ' +
181-
'for more details on how to use and troubleshoot this feature.';
182-
const errorMsg = `${error.error.message}; ${description}`;
183-
184-
throw FirebaseAuthError.fromServerError(errorCode, errorMsg, error);
185-
}
186-
throw new FirebaseAuthError(
187-
AuthClientErrorCode.INTERNAL_ERROR,
188-
'Error returned from server: ' + error + '. Additionally, an ' +
189-
'internal error occurred while attempting to extract the ' +
190-
'errorcode from the error.',
191-
);
192-
}
193-
throw err;
194-
});
195-
}
196-
197-
/**
198-
* @inheritDoc
199-
*/
200-
public getAccountId(): Promise<string> {
201-
if (validator.isNonEmptyString(this.serviceAccountId)) {
202-
return Promise.resolve(this.serviceAccountId);
203-
}
204-
const request: HttpRequestConfig = {
205-
method: 'GET',
206-
url: 'http://metadata/computeMetadata/v1/instance/service-accounts/default/email',
207-
headers: {
208-
'Metadata-Flavor': 'Google',
209-
},
210-
};
211-
const client = new HttpClient();
212-
return client.send(request).then((response) => {
213-
if (!response.text) {
214-
throw new FirebaseAuthError(
215-
AuthClientErrorCode.INTERNAL_ERROR,
216-
'HTTP Response missing payload',
217-
);
218-
}
219-
this.serviceAccountId = response.text;
220-
return response.text;
221-
}).catch((err) => {
222-
throw new FirebaseAuthError(
223-
AuthClientErrorCode.INVALID_CREDENTIAL,
224-
'Failed to determine service account. Make sure to initialize ' +
225-
'the SDK with a service account credential. Alternatively specify a service ' +
226-
`account with iam.serviceAccounts.signBlob permission. Original error: ${err}`,
227-
);
228-
});
229-
}
230-
}
231-
23263
/**
23364
* A CryptoSigner implementation that is used when communicating with the Auth emulator.
23465
* It produces unsigned tokens.
@@ -253,22 +84,6 @@ export class EmulatedSigner implements CryptoSigner {
25384
}
25485
}
25586

256-
/**
257-
* Create a new CryptoSigner instance for the given app. If the app has been initialized with a service
258-
* account credential, creates a ServiceAccountSigner. Otherwise creates an IAMSigner.
259-
*
260-
* @param {FirebaseApp} app A FirebaseApp instance.
261-
* @return {CryptoSigner} A CryptoSigner instance.
262-
*/
263-
export function cryptoSignerFromApp(app: FirebaseApp): CryptoSigner {
264-
const credential = app.options.credential;
265-
if (credential instanceof ServiceAccountCredential) {
266-
return new ServiceAccountSigner(credential);
267-
}
268-
269-
return new IAMSigner(new AuthorizedHttpClient(app), app.options.serviceAccountId);
270-
}
271-
27287
/**
27388
* Class for generating different types of Firebase Auth tokens (JWTs).
27489
*/
@@ -361,6 +176,8 @@ export class FirebaseTokenGenerator {
361176
return Promise.all([token, signPromise]);
362177
}).then(([token, signature]) => {
363178
return `${token}.${this.encodeSegment(signature)}`;
179+
}).catch((err) => {
180+
throw handleCryptoSignerError(err);
364181
});
365182
}
366183

@@ -383,3 +200,44 @@ export class FirebaseTokenGenerator {
383200
}
384201
}
385202

203+
/**
204+
* Creates a new FirebaseAuthError by extracting the error code, message and other relevant
205+
* details from a CryptoSignerError.
206+
*
207+
* @param {Error} err The Error to convert into a FirebaseAuthError error
208+
* @return {FirebaseAuthError} A Firebase Auth error that can be returned to the user.
209+
*/
210+
export function handleCryptoSignerError(err: Error): Error {
211+
if (!(err instanceof CryptoSignerError)) {
212+
return err;
213+
}
214+
if (err.code === CryptoSignerErrorCode.SERVER_ERROR && validator.isNonNullObject(err.cause)) {
215+
const httpError = err.cause;
216+
const errorResponse = (httpError as HttpError).response.data;
217+
if (validator.isNonNullObject(errorResponse) && errorResponse.error) {
218+
const errorCode = errorResponse.error.status;
219+
const description = 'Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens ' +
220+
'for more details on how to use and troubleshoot this feature.';
221+
const errorMsg = `${errorResponse.error.message}; ${description}`;
222+
223+
return FirebaseAuthError.fromServerError(errorCode, errorMsg, errorResponse);
224+
}
225+
return new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR,
226+
'Error returned from server: ' + errorResponse + '. Additionally, an ' +
227+
'internal error occurred while attempting to extract the ' +
228+
'errorcode from the error.'
229+
);
230+
}
231+
return new FirebaseAuthError(mapToAuthClientErrorCode(err.code), err.message);
232+
}
233+
234+
function mapToAuthClientErrorCode(code: string): ErrorInfo {
235+
switch (code) {
236+
case CryptoSignerErrorCode.INVALID_CREDENTIAL:
237+
return AuthClientErrorCode.INVALID_CREDENTIAL;
238+
case CryptoSignerErrorCode.INVALID_ARGUMENT:
239+
return AuthClientErrorCode.INVALID_ARGUMENT;
240+
default:
241+
return AuthClientErrorCode.INTERNAL_ERROR;
242+
}
243+
}

0 commit comments

Comments
 (0)