curl --request POST \
--url https://iam.basaltic.sh/v1/oauth/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data grant_type=client_credentials \
--data client_id=BYCLD1a2b3c4d5e6f7 \
--data 'client_secret=<string>' \
--data duration_seconds=3600 \
--data 'code=<string>' \
--data 'code_verifier=<string>' \
--data redirect_uri=urn:ietf:wg:oauth:2.0:oob \
--data 'refresh_token=<string>'import requests
url = "https://iam.basaltic.sh/v1/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": "BYCLD1a2b3c4d5e6f7",
"client_secret": "<string>",
"duration_seconds": "3600",
"code": "<string>",
"code_verifier": "<string>",
"redirect_uri": "urn:ietf:wg:oauth:2.0:oob",
"refresh_token": "<string>"
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post(url, data=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: 'BYCLD1a2b3c4d5e6f7',
client_secret: '<string>',
duration_seconds: '3600',
code: '<string>',
code_verifier: '<string>',
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
refresh_token: '<string>'
})
};
fetch('https://iam.basaltic.sh/v1/oauth/token', 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/oauth/token",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "grant_type=client_credentials&client_id=BYCLD1a2b3c4d5e6f7&client_secret=%3Cstring%3E&duration_seconds=3600&code=%3Cstring%3E&code_verifier=%3Cstring%3E&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&refresh_token=%3Cstring%3E",
CURLOPT_HTTPHEADER => [
"Content-Type: application/x-www-form-urlencoded"
],
]);
$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/oauth/token"
payload := strings.NewReader("grant_type=client_credentials&client_id=BYCLD1a2b3c4d5e6f7&client_secret=%3Cstring%3E&duration_seconds=3600&code=%3Cstring%3E&code_verifier=%3Cstring%3E&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&refresh_token=%3Cstring%3E")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
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/oauth/token")
.header("Content-Type", "application/x-www-form-urlencoded")
.body("grant_type=client_credentials&client_id=BYCLD1a2b3c4d5e6f7&client_secret=%3Cstring%3E&duration_seconds=3600&code=%3Cstring%3E&code_verifier=%3Cstring%3E&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&refresh_token=%3Cstring%3E")
.asString();require 'uri'
require 'net/http'
url = URI("https://iam.basaltic.sh/v1/oauth/token")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/x-www-form-urlencoded'
request.body = "grant_type=client_credentials&client_id=BYCLD1a2b3c4d5e6f7&client_secret=%3Cstring%3E&duration_seconds=3600&code=%3Cstring%3E&code_verifier=%3Cstring%3E&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&refresh_token=%3Cstring%3E"
response = http.request(request)
puts response.read_body{
"access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6Ii4uLiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "<string>"
}{
"error": "unsupported_grant_type",
"error_description": "unsupported grant type"
}{
"error": "invalid_client",
"error_description": "client authentication failed"
}{
"error": "invalid_grant",
"error_description": "Organization is suspended"
}{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests, please try again later",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": "temporarily_unavailable",
"error_description": "try again shortly"
}Intercambiar una clave de acceso por un token al portador
Intercambiar el par de claves de acceso de una cuenta de servicio por un token portador de corta duración, y luego enviar ese token como Authorization: Bearer <token> en cada otra llamada.
Esta es la forma habitual de autenticación. El par de claves de acceso sigue siendo la única credencial de larga duración que tiene una cuenta de servicio; lo que cambia es que presentas un token derivado de él en lugar de firmar cada solicitud.
curl -s -u "$KEY_ID:$SECRET" -d grant_type=client_credentials \
https://iam.basaltic.sh/v1/oauth/token
El mismo par de claves es también la credencial de AWS SigV4 para el punto final de objeto compatible con S3, lo que no dice nada más. Usa el token para esta API y el par de claves para S3; no es necesario elegir.
Los errores aquí usan la forma OAuth 2.0, no el sobre habitual de esta API — {"error": "...", "error_description": "..."} — porque cada biblioteca cliente OAuth analiza eso y nada más. Dos respuestas importan y sus remedios son opuestos. invalid_client significa que la clave fue rechazada: compruébela o rótela. invalid_grant significa que la clave está bien y la organización está suspendida o todavía en proceso de incorporación, donde rotar una clave de trabajo le haría perder tiempo.
Una clave de acceso desconocida y un secreto incorrecto responden a invalid_client con el mismo mensaje, por lo que el punto final no puede ser utilizado para descubrir qué claves existen.
curl --request POST \
--url https://iam.basaltic.sh/v1/oauth/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data grant_type=client_credentials \
--data client_id=BYCLD1a2b3c4d5e6f7 \
--data 'client_secret=<string>' \
--data duration_seconds=3600 \
--data 'code=<string>' \
--data 'code_verifier=<string>' \
--data redirect_uri=urn:ietf:wg:oauth:2.0:oob \
--data 'refresh_token=<string>'import requests
url = "https://iam.basaltic.sh/v1/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": "BYCLD1a2b3c4d5e6f7",
"client_secret": "<string>",
"duration_seconds": "3600",
"code": "<string>",
"code_verifier": "<string>",
"redirect_uri": "urn:ietf:wg:oauth:2.0:oob",
"refresh_token": "<string>"
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post(url, data=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: 'BYCLD1a2b3c4d5e6f7',
client_secret: '<string>',
duration_seconds: '3600',
code: '<string>',
code_verifier: '<string>',
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
refresh_token: '<string>'
})
};
fetch('https://iam.basaltic.sh/v1/oauth/token', 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/oauth/token",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "grant_type=client_credentials&client_id=BYCLD1a2b3c4d5e6f7&client_secret=%3Cstring%3E&duration_seconds=3600&code=%3Cstring%3E&code_verifier=%3Cstring%3E&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&refresh_token=%3Cstring%3E",
CURLOPT_HTTPHEADER => [
"Content-Type: application/x-www-form-urlencoded"
],
]);
$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/oauth/token"
payload := strings.NewReader("grant_type=client_credentials&client_id=BYCLD1a2b3c4d5e6f7&client_secret=%3Cstring%3E&duration_seconds=3600&code=%3Cstring%3E&code_verifier=%3Cstring%3E&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&refresh_token=%3Cstring%3E")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
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/oauth/token")
.header("Content-Type", "application/x-www-form-urlencoded")
.body("grant_type=client_credentials&client_id=BYCLD1a2b3c4d5e6f7&client_secret=%3Cstring%3E&duration_seconds=3600&code=%3Cstring%3E&code_verifier=%3Cstring%3E&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&refresh_token=%3Cstring%3E")
.asString();require 'uri'
require 'net/http'
url = URI("https://iam.basaltic.sh/v1/oauth/token")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/x-www-form-urlencoded'
request.body = "grant_type=client_credentials&client_id=BYCLD1a2b3c4d5e6f7&client_secret=%3Cstring%3E&duration_seconds=3600&code=%3Cstring%3E&code_verifier=%3Cstring%3E&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&refresh_token=%3Cstring%3E"
response = http.request(request)
puts response.read_body{
"access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6Ii4uLiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "<string>"
}{
"error": "unsupported_grant_type",
"error_description": "unsupported grant type"
}{
"error": "invalid_client",
"error_description": "client authentication failed"
}{
"error": "invalid_grant",
"error_description": "Organization is suspended"
}{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests, please try again later",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": "temporarily_unavailable",
"error_description": "try again shortly"
}Cuerpo
Una solicitud de token OAuth 2.0. La codificación de formularios es lo que especifica RFC 6749 y lo que envían las bibliotecas cliente; también se acepta JSON.
Las credenciales del cliente pueden ser enviadas como HTTP Basic (Authorization: Basic base64(key_id:secret), que es lo que la mayoría de las bibliotecas hacen por defecto) o como client_id y client_secret campos. Básico gana si ambos están presentes.
client_credentials es el que se usa para una cuenta de servicio: intercambia un par de claves de acceso por un token, y no necesita nada más.
authorization_code y refresh_token pertenecen al login interactivo que ejecuta una persona (basaltic login), donde el token nombra a un USER en lugar de una cuenta de servicio. Son impulsados por la CLI, no escritos a mano. Compruebe el documento de metadatos del servidor de autorización antes de ramificarse en ellos; solo se anuncian cuando se configura un punto final de autorización.
client_credentials, authorization_code, refresh_token "client_credentials"
El id de la clave de acceso. Omita cuando use HTTP Basic.
"BYCLD1a2b3c4d5e6f7"
La clave de acceso secreta. Omita cuando use HTTP Basic.
Vida útil del token solicitado. Una extensión de Basaltic, no un parámetro de OAuth — omítelo y obtendrás el valor predeterminado. Los valores fuera del rango se fijan en él en lugar de rechazarse, por lo que pedir un día produce el token más largo permitido.
900 <= x <= 432003600
El código de autorización de la redirección de consentimiento. Uso único, y válido por cinco minutos. authorization_code concede solo.
El verificador PKCE cuyo SHA-256 fue enviado como code_challenge cuando el flujo comenzó (RFC 7636). Requerido con authorization_code: es lo que prueba que este es el cliente que inició el flujo, ya que una CLI no tiene secreto de cliente.
El mismo redirect_uri para el que se emitió el código — para la CLI, urn:ietf:wg:oauth:2.0:oob. Se vuelve a comprobar aquí, por lo que un código no se puede canjear bajo otro diferente (RFC 6749 4.1.3).
"urn:ietf:wg:oauth:2.0:oob"
refresh_token concede solo. Renueva una sesión de usuario sin otro viaje a través del navegador. Rotar en cada uso - guardar el nuevo.
Respuesta
Un token portador
Respuesta de token RFC 6749.
Enviar como Authorization: Bearer <token>. Opaca para los clientes: no la analiza, y no introduce nada en la cadena de token.
"eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6Ii4uLiJ9..."
Bearer "Bearer"
Segundos hasta que caduque el token.
3600
Devuelto solo por las concesiones del usuario (authorization_code y refresh_token). Presente la licencia refresh_token para renovarla sin otro viaje de ida y vuelta del navegador; se ROTA en cada uso, por lo que reemplaza la copia almacenada cada vez.
Una cuenta de servicio no obtiene ninguna. Ya tiene una clave de acceso de larga duración y puede simplemente ejecutar client_credentials de nuevo, por lo que un token de actualización sería una segunda credencial para almacenar sin ningún beneficio.

