minio
Version:
S3 Compatible Cloud Storage client
263 lines (223 loc) • 7.21 kB
text/typescript
import * as http from 'node:http'
import * as https from 'node:https'
import { URL, URLSearchParams } from 'node:url'
import { CredentialProvider } from './CredentialProvider.ts'
import { Credentials } from './Credentials.ts'
import { makeDateLong, parseXml, toSha256 } from './internal/helper.ts'
import { request } from './internal/request.ts'
import { readAsString } from './internal/response.ts'
import type { Transport } from './internal/type.ts'
import { signV4ByServiceName } from './signing.ts'
/**
* @see https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
*/
type CredentialResponse = {
ErrorResponse?: {
Error?: {
Code?: string
Message?: string
}
}
AssumeRoleResponse: {
AssumeRoleResult: {
Credentials: {
AccessKeyId: string
SecretAccessKey: string
SessionToken: string
Expiration: string
}
}
}
}
export interface AssumeRoleProviderOptions {
stsEndpoint: string
accessKey: string
secretKey: string
durationSeconds?: number
sessionToken?: string
policy?: string
region?: string
roleArn?: string
roleSessionName?: string
externalId?: string
token?: string
webIdentityToken?: string
action?: string
transportAgent?: http.Agent
}
const defaultExpirySeconds = 900
export class AssumeRoleProvider extends CredentialProvider {
private readonly stsEndpoint: URL
private readonly accessKey: string
private readonly secretKey: string
private readonly durationSeconds: number
private readonly policy?: string
private readonly region: string
private readonly roleArn?: string
private readonly roleSessionName?: string
private readonly externalId?: string
private readonly token?: string
private readonly webIdentityToken?: string
private readonly action: string
private _credentials: Credentials | null
private readonly expirySeconds: number
private accessExpiresAt = ''
private readonly transportAgent?: http.Agent
private readonly transport: Transport
constructor({
stsEndpoint,
accessKey,
secretKey,
durationSeconds = defaultExpirySeconds,
sessionToken,
policy,
region = '',
roleArn,
roleSessionName,
externalId,
token,
webIdentityToken,
action = 'AssumeRole',
transportAgent = undefined,
}: AssumeRoleProviderOptions) {
super({ accessKey, secretKey, sessionToken })
this.stsEndpoint = new URL(stsEndpoint)
this.accessKey = accessKey
this.secretKey = secretKey
this.policy = policy
this.region = region
this.roleArn = roleArn
this.roleSessionName = roleSessionName
this.externalId = externalId
this.token = token
this.webIdentityToken = webIdentityToken
this.action = action
this.durationSeconds = parseInt(durationSeconds as unknown as string)
let expirySeconds = this.durationSeconds
if (this.durationSeconds < defaultExpirySeconds) {
expirySeconds = defaultExpirySeconds
}
this.expirySeconds = expirySeconds // for calculating refresh of credentials.
// By default, nodejs uses a global agent if the 'agent' property
// is set to undefined. Otherwise, it's okay to assume the users
// know what they're doing if they specify a custom transport agent.
this.transportAgent = transportAgent
const isHttp: boolean = this.stsEndpoint.protocol === 'http:'
this.transport = isHttp ? http : https
/**
* Internal Tracking variables
*/
this._credentials = null
}
getRequestConfig(): {
requestOptions: http.RequestOptions
requestData: string
} {
const hostValue = this.stsEndpoint.hostname
const portValue = this.stsEndpoint.port
const qryParams = new URLSearchParams({ Action: this.action, Version: '2011-06-15' })
qryParams.set('DurationSeconds', this.expirySeconds.toString())
if (this.policy) {
qryParams.set('Policy', this.policy)
}
if (this.roleArn) {
qryParams.set('RoleArn', this.roleArn)
}
if (this.roleSessionName != null) {
qryParams.set('RoleSessionName', this.roleSessionName)
}
if (this.token != null) {
qryParams.set('Token', this.token)
}
if (this.webIdentityToken) {
qryParams.set('WebIdentityToken', this.webIdentityToken)
}
if (this.externalId) {
qryParams.set('ExternalId', this.externalId)
}
const urlParams = qryParams.toString()
const contentSha256 = toSha256(urlParams)
const date = new Date()
const requestOptions = {
hostname: hostValue,
port: portValue,
path: '/',
protocol: this.stsEndpoint.protocol,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'content-length': urlParams.length.toString(),
host: hostValue,
'x-amz-date': makeDateLong(date),
'x-amz-content-sha256': contentSha256,
} as Record<string, string>,
agent: this.transportAgent,
} satisfies http.RequestOptions
requestOptions.headers.authorization = signV4ByServiceName(
requestOptions,
this.accessKey,
this.secretKey,
this.region,
date,
contentSha256,
'sts',
)
return {
requestOptions,
requestData: urlParams,
}
}
async performRequest(): Promise<CredentialResponse> {
const { requestOptions, requestData } = this.getRequestConfig()
const res = await request(this.transport, requestOptions, requestData)
const body = await readAsString(res)
return parseXml(body)
}
parseCredentials(respObj: CredentialResponse): Credentials {
if (respObj.ErrorResponse) {
throw new Error(
`Unable to obtain credentials: ${respObj.ErrorResponse?.Error?.Code} ${respObj.ErrorResponse?.Error?.Message}`,
{ cause: respObj },
)
}
const {
AssumeRoleResponse: {
AssumeRoleResult: {
Credentials: {
AccessKeyId: accessKey,
SecretAccessKey: secretKey,
SessionToken: sessionToken,
Expiration: expiresAt,
},
},
},
} = respObj
this.accessExpiresAt = expiresAt
return new Credentials({ accessKey, secretKey, sessionToken })
}
async refreshCredentials(): Promise<Credentials> {
try {
const assumeRoleCredentials = await this.performRequest()
this._credentials = this.parseCredentials(assumeRoleCredentials)
} catch (err) {
throw new Error(`Failed to get Credentials: ${err}`, { cause: err })
}
return this._credentials
}
async getCredentials(): Promise<Credentials> {
if (this._credentials && !this.isAboutToExpire()) {
return this._credentials
}
this._credentials = await this.refreshCredentials()
return this._credentials
}
isAboutToExpire() {
const expiresAt = new Date(this.accessExpiresAt)
const provisionalExpiry = new Date(Date.now() + 1000 * 10) // check before 10 seconds.
return provisionalExpiry > expiresAt
}
}
// deprecated default export, please use named exports.
// keep for backward compatibility.
// eslint-disable-next-line import/no-default-export
export default AssumeRoleProvider