( credentialId: string, scopes: string[], impersonateEmail?: string )
| 124 | * authenticates directly with its own IAM permissions. |
| 125 | */ |
| 126 | export async function getServiceAccountToken( |
| 127 | credentialId: string, |
| 128 | scopes: string[], |
| 129 | impersonateEmail?: string |
| 130 | ): Promise<string> { |
| 131 | const [credentialRow] = await db |
| 132 | .select({ |
| 133 | encryptedServiceAccountKey: credential.encryptedServiceAccountKey, |
| 134 | }) |
| 135 | .from(credential) |
| 136 | .where(eq(credential.id, credentialId)) |
| 137 | .limit(1) |
| 138 | |
| 139 | if (!credentialRow?.encryptedServiceAccountKey) { |
| 140 | throw new Error('Service account key not found') |
| 141 | } |
| 142 | |
| 143 | const { decrypted } = await decryptSecret(credentialRow.encryptedServiceAccountKey) |
| 144 | const keyData = JSON.parse(decrypted) as { |
| 145 | client_email: string |
| 146 | private_key: string |
| 147 | token_uri?: string |
| 148 | } |
| 149 | |
| 150 | const filteredScopes = scopes.filter((s) => !SA_EXCLUDED_SCOPES.has(s)) |
| 151 | |
| 152 | const now = Math.floor(Date.now() / 1000) |
| 153 | const ALLOWED_TOKEN_URIS = new Set(['https://oauth2.googleapis.com/token']) |
| 154 | const tokenUri = |
| 155 | keyData.token_uri && ALLOWED_TOKEN_URIS.has(keyData.token_uri) |
| 156 | ? keyData.token_uri |
| 157 | : 'https://oauth2.googleapis.com/token' |
| 158 | |
| 159 | const header = { alg: 'RS256', typ: 'JWT' } |
| 160 | const payload: Record<string, unknown> = { |
| 161 | iss: keyData.client_email, |
| 162 | scope: filteredScopes.join(' '), |
| 163 | aud: tokenUri, |
| 164 | iat: now, |
| 165 | exp: now + 3600, |
| 166 | } |
| 167 | |
| 168 | if (impersonateEmail) { |
| 169 | payload.sub = impersonateEmail |
| 170 | } |
| 171 | |
| 172 | logger.info('Service account JWT payload', { |
| 173 | iss: keyData.client_email, |
| 174 | sub: impersonateEmail || '(none)', |
| 175 | scopes: filteredScopes.join(' '), |
| 176 | aud: tokenUri, |
| 177 | }) |
| 178 | |
| 179 | const toBase64Url = (obj: unknown) => Buffer.from(JSON.stringify(obj)).toString('base64url') |
| 180 | |
| 181 | const signingInput = `${toBase64Url(header)}.${toBase64Url(payload)}` |
| 182 | |
| 183 | const signer = createSign('RSA-SHA256') |
no test coverage detected