curl --request POST \
--url https://iam.basaltic.sh/v1/assume-role-with-web-identity \
--header 'Content-Type: application/json' \
--data '
{
"web_identity_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"role": "crn:iam::production:role/github-deployer",
"session_name": "reports-exporter",
"duration_seconds": 3600
}
'import requests
url = "https://iam.basaltic.sh/v1/assume-role-with-web-identity"
payload = {
"web_identity_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"role": "crn:iam::production:role/github-deployer",
"session_name": "reports-exporter",
"duration_seconds": 3600
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
web_identity_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6...',
role: 'crn:iam::production:role/github-deployer',
session_name: 'reports-exporter',
duration_seconds: 3600
})
};
fetch('https://iam.basaltic.sh/v1/assume-role-with-web-identity', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://iam.basaltic.sh/v1/assume-role-with-web-identity",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'web_identity_token' => 'eyJhbGciOiJSUzI1NiIsImtpZCI6...',
'role' => 'crn:iam::production:role/github-deployer',
'session_name' => 'reports-exporter',
'duration_seconds' => 3600
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://iam.basaltic.sh/v1/assume-role-with-web-identity"
payload := strings.NewReader("{\n \"web_identity_token\": \"eyJhbGciOiJSUzI1NiIsImtpZCI6...\",\n \"role\": \"crn:iam::production:role/github-deployer\",\n \"session_name\": \"reports-exporter\",\n \"duration_seconds\": 3600\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://iam.basaltic.sh/v1/assume-role-with-web-identity")
.header("Content-Type", "application/json")
.body("{\n \"web_identity_token\": \"eyJhbGciOiJSUzI1NiIsImtpZCI6...\",\n \"role\": \"crn:iam::production:role/github-deployer\",\n \"session_name\": \"reports-exporter\",\n \"duration_seconds\": 3600\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://iam.basaltic.sh/v1/assume-role-with-web-identity")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"web_identity_token\": \"eyJhbGciOiJSUzI1NiIsImtpZCI6...\",\n \"role\": \"crn:iam::production:role/github-deployer\",\n \"session_name\": \"reports-exporter\",\n \"duration_seconds\": 3600\n}"
response = http.request(request)
puts response.read_body{
"access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6Ii4uLiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"access_key_id": "AKIA...",
"secret_access_key": "wJalrXUtnFEMI...",
"session_token": "FwoGZXIvYXdzE...",
"expiration": "2026-01-15T09:30:00Z",
"account_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"account_handle": "<string>",
"role_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}{
"error": {
"code": "INVALID_INPUT",
"message": "Invalid request parameters",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "invalid web identity token",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "IAM_CANNOT_ASSUME_ROLE",
"message": "You are not authorized to assume this role",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "NOT_FOUND",
"message": "Resource not found",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests, please try again later",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "INTERNAL_ERROR",
"message": "An internal error occurred",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "NOT_IMPLEMENTED",
"message": "web identity federation is not configured",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "The service is temporarily unavailable. Please try again later.",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}Assume role with web identity
Exchange a signed OIDC token for temporary role credentials. No existing Basaltic credentials are required. The full role CRN determines the account; there is no separate account selector.
Register the token’s issuer through /v1/oidc-providers in the same account as the role. The token’s signature, issuer, allowed audience and expiry are verified before reading the role. The role trust policy must name the provider CRN and satisfy every condition. Use basalt:webidentity:Subject for sub and basalt:webidentity:Audience for aud. GitHub trust must include a positive subject restriction to the intended organization or repository.
Provider registration alone grants no access. Invalid tokens return 401; a verified token refused by role trust returns 403. Requests are rate-limited per client IP. No inline session policy is accepted.
curl --request POST \
--url https://iam.basaltic.sh/v1/assume-role-with-web-identity \
--header 'Content-Type: application/json' \
--data '
{
"web_identity_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"role": "crn:iam::production:role/github-deployer",
"session_name": "reports-exporter",
"duration_seconds": 3600
}
'import requests
url = "https://iam.basaltic.sh/v1/assume-role-with-web-identity"
payload = {
"web_identity_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"role": "crn:iam::production:role/github-deployer",
"session_name": "reports-exporter",
"duration_seconds": 3600
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
web_identity_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6...',
role: 'crn:iam::production:role/github-deployer',
session_name: 'reports-exporter',
duration_seconds: 3600
})
};
fetch('https://iam.basaltic.sh/v1/assume-role-with-web-identity', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://iam.basaltic.sh/v1/assume-role-with-web-identity",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'web_identity_token' => 'eyJhbGciOiJSUzI1NiIsImtpZCI6...',
'role' => 'crn:iam::production:role/github-deployer',
'session_name' => 'reports-exporter',
'duration_seconds' => 3600
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://iam.basaltic.sh/v1/assume-role-with-web-identity"
payload := strings.NewReader("{\n \"web_identity_token\": \"eyJhbGciOiJSUzI1NiIsImtpZCI6...\",\n \"role\": \"crn:iam::production:role/github-deployer\",\n \"session_name\": \"reports-exporter\",\n \"duration_seconds\": 3600\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://iam.basaltic.sh/v1/assume-role-with-web-identity")
.header("Content-Type", "application/json")
.body("{\n \"web_identity_token\": \"eyJhbGciOiJSUzI1NiIsImtpZCI6...\",\n \"role\": \"crn:iam::production:role/github-deployer\",\n \"session_name\": \"reports-exporter\",\n \"duration_seconds\": 3600\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://iam.basaltic.sh/v1/assume-role-with-web-identity")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"web_identity_token\": \"eyJhbGciOiJSUzI1NiIsImtpZCI6...\",\n \"role\": \"crn:iam::production:role/github-deployer\",\n \"session_name\": \"reports-exporter\",\n \"duration_seconds\": 3600\n}"
response = http.request(request)
puts response.read_body{
"access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6Ii4uLiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"access_key_id": "AKIA...",
"secret_access_key": "wJalrXUtnFEMI...",
"session_token": "FwoGZXIvYXdzE...",
"expiration": "2026-01-15T09:30:00Z",
"account_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"account_handle": "<string>",
"role_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}{
"error": {
"code": "INVALID_INPUT",
"message": "Invalid request parameters",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "invalid web identity token",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "IAM_CANNOT_ASSUME_ROLE",
"message": "You are not authorized to assume this role",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "NOT_FOUND",
"message": "Resource not found",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests, please try again later",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "INTERNAL_ERROR",
"message": "An internal error occurred",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "NOT_IMPLEMENTED",
"message": "web identity federation is not configured",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "The service is temporarily unavailable. Please try again later.",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}Body
Exchange a signed OIDC token for credentials in the account identified by the full role CRN.
Signed OIDC JWT. The issuer must be registered in the role account; signature, expiry and an allowed audience are verified before evaluating role trust.
"eyJhbGciOiJSUzI1NiIsImtpZCI6..."
Full account-scoped role CRN. Bare UUIDs and names are not accepted.
"crn:iam::production:role/github-deployer"
A label recorded on the session and in the audit trail. Defaults to the
token's sub claim, so an unnamed session still records which identity
it came from.
"reports-exporter"
Credential validity duration (15 min to 12 hours). A value above the
role's own max_session_duration is rejected rather than clamped.
900 <= x <= 432003600
Response
Role assumed successfully
A role session's credentials, in both forms it can be presented.
access_token is a bearer token for this API — send it as
Authorization: Bearer <token>. The other four fields are AWS SigV4
credentials for the S3-compatible object storage endpoint, which speaks
nothing else.
Both come from the same session and share its expiry, so revoking the session stops both at once. Use whichever the endpoint you are calling needs; there is no need to choose one at request time.
Bearer token for the Basaltic API. Present on every role session.
"eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6Ii4uLiJ9..."
Always Bearer when access_token is present.
"Bearer"
Seconds until access_token expires.
3600
SigV4 access key id, for the S3 endpoint.
"AKIA..."
SigV4 secret, for the S3 endpoint.
"wJalrXUtnFEMI..."
SigV4 session token, for the S3 endpoint. Send as
X-Amz-Security-Token and include it in SignedHeaders.
"FwoGZXIvYXdzE..."
When the session — and therefore both credential forms — expires.
"2026-01-15T09:30:00Z"
Owning account UUID of the target role.
Owning account handle of the target role.
Immutable UUID of the assumed role.

