The Security Token Service (STS) provides temporary credentials for secure, time-limited access to OSS resources. It enables fine-grained access control through role assumption and policy-based permissions.
function STS(options: STSOptions): STSClient;
interface STSOptions {
accessKeyId: string;
accessKeySecret: string;
endpoint?: string;
apiVersion?: string;
timeout?: number | number[];
}const OSS = require('ali-oss');
// Create STS client
const sts = new OSS.STS({
accessKeyId: 'your-access-key-id',
accessKeySecret: 'your-access-key-secret'
});async function assumeRole(role: string, policy?: string, expiration?: number, session?: string, options?: RequestOptions): Promise<AssumeRoleResult>;
interface AssumeRoleResult {
credentials: STSCredentials;
assumedRoleUser: AssumedRoleUser;
res: ResponseInfo;
}
interface STSCredentials {
accessKeyId: string;
accessKeySecret: string;
securityToken: string;
expiration: Date;
}
interface AssumedRoleUser {
arn: string;
assumedRoleId: string;
}acs:ram::123456789012:role/MyRole)The STS service validates policy documents as JSON strings. Policies must:
// Assume a role for temporary access
const result = await sts.assumeRole(
'acs:ram::123456789012:role/OSS-ReadOnly',
null, // No additional policy restrictions
3600, // 1 hour expiration
'MySession' // Session identifier
);
console.log('Temporary credentials:');
console.log('Access Key ID:', result.credentials.accessKeyId);
console.log('Access Key Secret:', result.credentials.accessKeySecret);
console.log('Security Token:', result.credentials.securityToken);
console.log('Expires at:', result.credentials.expiration);
// Use temporary credentials with OSS client
const ossClient = new OSS({
region: 'oss-cn-hangzhou',
accessKeyId: result.credentials.accessKeyId,
accessKeySecret: result.credentials.accessKeySecret,
stsToken: result.credentials.securityToken,
bucket: 'my-bucket'
});// Define a restrictive policy
const restrictivePolicy = {
Version: '1',
Statement: [
{
Effect: 'Allow',
Action: ['oss:GetObject'],
Resource: ['acs:oss:*:*:my-bucket/public/*']
}
]
};
// Assume role with additional restrictions
const result = await sts.assumeRole(
'acs:ram::123456789012:role/OSS-FullAccess',
JSON.stringify(restrictivePolicy),
1800, // 30 minutes
'RestrictedSession'
);
// This client can only read objects in the 'public/' prefix
const restrictedClient = new OSS({
region: 'oss-cn-hangzhou',
accessKeyId: result.credentials.accessKeyId,
accessKeySecret: result.credentials.accessKeySecret,
stsToken: result.credentials.securityToken,
bucket: 'my-bucket'
});// Policy for upload-only access to specific prefix
const uploadPolicy = {
Version: '1',
Statement: [
{
Effect: 'Allow',
Action: ['oss:PutObject'],
Resource: [`acs:oss:*:*:my-bucket/uploads/${userId}/*`]
}
]
};
const uploadCredentials = await sts.assumeRole(
'acs:ram::123456789012:role/OSS-Upload',
JSON.stringify(uploadPolicy),
900, // 15 minutes for upload
`upload-${userId}`
);
// Client with upload-only permissions
const uploadClient = new OSS({
region: 'oss-cn-hangzhou',
accessKeyId: uploadCredentials.credentials.accessKeyId,
accessKeySecret: uploadCredentials.credentials.accessKeySecret,
stsToken: uploadCredentials.credentials.securityToken,
bucket: 'my-bucket'
});// Policy for read-only access to specific objects
const downloadPolicy = {
Version: '1',
Statement: [
{
Effect: 'Allow',
Action: ['oss:GetObject'],
Resource: [
'acs:oss:*:*:my-bucket/documents/report.pdf',
'acs:oss:*:*:my-bucket/images/photo.jpg'
]
}
]
};
const downloadCredentials = await sts.assumeRole(
'acs:ram::123456789012:role/OSS-ReadOnly',
JSON.stringify(downloadPolicy),
3600, // 1 hour
`download-${sessionId}`
);// Assume role in different account
const crossAccountCredentials = await sts.assumeRole(
'acs:ram::987654321098:role/CrossAccountAccess',
null,
7200, // 2 hours
'CrossAccountSession'
);
// Access resources in different account
const crossAccountClient = new OSS({
region: 'oss-cn-beijing',
accessKeyId: crossAccountCredentials.credentials.accessKeyId,
accessKeySecret: crossAccountCredentials.credentials.accessKeySecret,
stsToken: crossAccountCredentials.credentials.securityToken,
bucket: 'partner-bucket'
});class STSTokenManager {
constructor(stsClient, role, policy, expiration = 3600) {
this.stsClient = stsClient;
this.role = role;
this.policy = policy;
this.expirationSeconds = expiration;
this.credentials = null;
this.refreshTimer = null;
}
async getCredentials() {
if (!this.credentials || this.isExpiringSoon()) {
await this.refreshCredentials();
}
return this.credentials;
}
async refreshCredentials() {
try {
const result = await this.stsClient.assumeRole(
this.role,
this.policy,
this.expirationSeconds,
`session-${Date.now()}`
);
this.credentials = result.credentials;
this.scheduleRefresh();
console.log('STS credentials refreshed');
} catch (error) {
console.error('Failed to refresh STS credentials:', error);
throw error;
}
}
isExpiringSoon(bufferMinutes = 5) {
if (!this.credentials) return true;
const expirationTime = new Date(this.credentials.expiration);
const now = new Date();
const bufferMs = bufferMinutes * 60 * 1000;
return (expirationTime.getTime() - now.getTime()) < bufferMs;
}
scheduleRefresh() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
}
// Refresh 10 minutes before expiration
const refreshTime = (this.expirationSeconds - 600) * 1000;
this.refreshTimer = setTimeout(() => {
this.refreshCredentials().catch(console.error);
}, refreshTime);
}
cleanup() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
}
}
}
// Usage
const tokenManager = new STSTokenManager(
sts,
'acs:ram::123456789012:role/OSS-ReadWrite',
null,
3600
);
// Get current credentials (will refresh if needed)
const credentials = await tokenManager.getCredentials();
const ossClient = new OSS({
region: 'oss-cn-hangzhou',
accessKeyId: credentials.accessKeyId,
accessKeySecret: credentials.accessKeySecret,
stsToken: credentials.securityToken,
bucket: 'my-bucket'
});// Server-side: Generate STS token for browser use
async function generateBrowserSTS(userId, permissions) {
const policy = {
Version: '1',
Statement: [
{
Effect: 'Allow',
Action: permissions.actions,
Resource: permissions.resources,
Condition: {
StringEquals: {
'oss:x-oss-meta-user-id': userId
}
}
}
]
};
const result = await sts.assumeRole(
'acs:ram::123456789012:role/BrowserAccess',
JSON.stringify(policy),
1800, // 30 minutes for browser
`browser-${userId}`
);
// Return only necessary information to browser
return {
accessKeyId: result.credentials.accessKeyId,
accessKeySecret: result.credentials.accessKeySecret,
securityToken: result.credentials.securityToken,
expiration: result.credentials.expiration,
region: 'oss-cn-hangzhou',
bucket: 'user-uploads'
};
}
// Client-side: Use STS credentials
const browserCredentials = await fetch('/api/sts-token').then(r => r.json());
const browserOSS = new OSS({
region: browserCredentials.region,
accessKeyId: browserCredentials.accessKeyId,
accessKeySecret: browserCredentials.accessKeySecret,
stsToken: browserCredentials.securityToken,
bucket: browserCredentials.bucket
});const readOnlyPolicy = {
Version: '1',
Statement: [
{
Effect: 'Allow',
Action: [
'oss:GetObject',
'oss:GetObjectMeta',
'oss:ListObjects'
],
Resource: ['acs:oss:*:*:my-bucket/*']
}
]
};const uploadOnlyPolicy = {
Version: '1',
Statement: [
{
Effect: 'Allow',
Action: ['oss:PutObject'],
Resource: ['acs:oss:*:*:my-bucket/uploads/*'],
Condition: {
StringEquals: {
'oss:x-oss-object-type': 'Normal'
}
}
}
]
};const timeBasedPolicy = {
Version: '1',
Statement: [
{
Effect: 'Allow',
Action: ['oss:GetObject'],
Resource: ['acs:oss:*:*:my-bucket/*'],
Condition: {
DateGreaterThan: {
'acs:CurrentTime': '2024-01-01T00:00:00Z'
},
DateLessThan: {
'acs:CurrentTime': '2024-12-31T23:59:59Z'
}
}
}
]
};const ipRestrictedPolicy = {
Version: '1',
Statement: [
{
Effect: 'Allow',
Action: ['oss:*'],
Resource: ['acs:oss:*:*:my-bucket/*'],
Condition: {
IpAddress: {
'acs:SourceIp': ['192.168.1.0/24', '10.0.0.0/8']
}
}
}
]
};try {
const result = await sts.assumeRole(role, policy, expiration, session);
return result.credentials;
} catch (error) {
if (error.code === 'NoSuchRole') {
console.error('Role does not exist or access denied');
} else if (error.code === 'MalformedPolicyDocument') {
console.error('Invalid policy format');
} else if (error.code === 'InvalidParameterValue') {
console.error('Invalid parameter (check expiration time, role ARN format)');
} else if (error.code === 'TokenRequestDenied') {
console.error('Permission denied for role assumption');
} else {
console.error('STS error:', error.message);
}
throw error;
}function validateCredentials(credentials) {
if (!credentials) {
throw new Error('No credentials provided');
}
if (!credentials.accessKeyId || !credentials.accessKeySecret || !credentials.securityToken) {
throw new Error('Incomplete credentials');
}
const now = new Date();
const expiration = new Date(credentials.expiration);
if (expiration <= now) {
throw new Error('Credentials have expired');
}
return true;
}The STS service uses HMAC-SHA1 signature algorithm for request authentication:
STS requests include:
Common STS error codes:
Retry behavior includes exponential backoff for transient errors.
// Grant minimal necessary permissions
const minimalPolicy = {
Version: '1',
Statement: [
{
Effect: 'Allow',
Action: ['oss:GetObject'], // Only what's needed
Resource: [`acs:oss:*:*:bucket/user-${userId}/*`], // Specific resources
Condition: {
StringEquals: {
'oss:x-oss-meta-owner': userId // Additional restrictions
}
}
}
]
};// Use short expiration times
const credentials = await sts.assumeRole(
role,
policy,
900, // 15 minutes minimum, use for specific operations
session
);// Use descriptive session names for auditing
const sessionName = `${operation}-${userId}-${timestamp}`;
const credentials = await sts.assumeRole(role, policy, expiration, sessionName);// Never store STS tokens in persistent storage
// Use in-memory storage with automatic refresh
class SecureTokenStorage {
constructor() {
this.tokens = new Map();
}
set(key, credentials) {
// Store with expiration check
this.tokens.set(key, {
credentials,
storedAt: new Date()
});
// Auto-cleanup expired tokens
setTimeout(() => {
this.tokens.delete(key);
}, this.getTimeToExpiration(credentials));
}
get(key) {
const entry = this.tokens.get(key);
if (!entry) return null;
// Check if still valid
const now = new Date();
const expiration = new Date(entry.credentials.expiration);
if (expiration <= now) {
this.tokens.delete(key);
return null;
}
return entry.credentials;
}
getTimeToExpiration(credentials) {
const now = new Date();
const expiration = new Date(credentials.expiration);
return Math.max(0, expiration.getTime() - now.getTime());
}
}