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
2323import * as validator from '../utils/validator' ;
2424import { toWebSafeBase64 } from '../utils' ;
2525import { Algorithm } from 'jsonwebtoken' ;
26+ import { HttpError } from '../utils/api-request' ;
2627
27-
28- const ALGORITHM_RS256 : Algorithm = 'RS256' as const ;
2928const ALGORITHM_NONE : Algorithm = 'none' as const ;
3029
3130const ONE_HOUR_IN_SECONDS = 60 * 60 ;
@@ -39,32 +38,6 @@ export const BLACKLISTED_CLAIMS = [
3938// Audience to use for Firebase Auth Custom tokens
4039const 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