curl --request POST \
--url https://iam.basaltic.sh/v1/oauth/authorize \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"client_id": "basaltic-cli",
"redirect_uri": "http://127.0.0.1:53682/callback",
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"code_challenge_method": "S256",
"organization_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"state": "<string>"
}
'import requests
url = "https://iam.basaltic.sh/v1/oauth/authorize"
payload = {
"client_id": "basaltic-cli",
"redirect_uri": "http://127.0.0.1:53682/callback",
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"code_challenge_method": "S256",
"organization_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"state": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
client_id: 'basaltic-cli',
redirect_uri: 'http://127.0.0.1:53682/callback',
code_challenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM',
code_challenge_method: 'S256',
organization_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
state: '<string>'
})
};
fetch('https://iam.basaltic.sh/v1/oauth/authorize', 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/authorize",
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([
'client_id' => 'basaltic-cli',
'redirect_uri' => 'http://127.0.0.1:53682/callback',
'code_challenge' => 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM',
'code_challenge_method' => 'S256',
'organization_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'state' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"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/oauth/authorize"
payload := strings.NewReader("{\n \"client_id\": \"basaltic-cli\",\n \"redirect_uri\": \"http://127.0.0.1:53682/callback\",\n \"code_challenge\": \"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM\",\n \"code_challenge_method\": \"S256\",\n \"organization_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"state\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
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/oauth/authorize")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"client_id\": \"basaltic-cli\",\n \"redirect_uri\": \"http://127.0.0.1:53682/callback\",\n \"code_challenge\": \"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM\",\n \"code_challenge_method\": \"S256\",\n \"organization_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"state\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://iam.basaltic.sh/v1/oauth/authorize")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"client_id\": \"basaltic-cli\",\n \"redirect_uri\": \"http://127.0.0.1:53682/callback\",\n \"code_challenge\": \"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM\",\n \"code_challenge_method\": \"S256\",\n \"organization_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"state\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"redirect_to": "http://127.0.0.1:53682/callback?code=...&state=..."
}{
"error": {
"code": "INVALID_INPUT",
"message": "Invalid request parameters",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Authentication required",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "ACCESS_DENIED",
"message": "You don't have permission to perform this action",
"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"
}
}Approve a CLI login and issue an authorization code
Approve a client to act as you, and receive the redirect that hands it an authorization code.
This is the console’s endpoint, not a client’s. It is called by the Basaltic console’s consent page on behalf of a signed-in user; the CLI never calls it. A CLI opens a browser at that page, and the page calls this. Anything driving it directly would need the user’s console session, at which point it already has everything the code would grant.
It is the half of the authorization-code flow that establishes WHO is
approving. The user must already be signed in — including any second
factor — and must be a member of the organization named in
organization_id. The organization is explicit rather than inferred: a
person in several has no single obvious answer, and choosing one for them
would scope the resulting token to something they did not pick.
Unlike the token endpoint, this answers in the usual API envelope. It is not part of the surface a third-party OAuth client talks to, so it follows the caller — and the caller is our own front end.
curl --request POST \
--url https://iam.basaltic.sh/v1/oauth/authorize \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"client_id": "basaltic-cli",
"redirect_uri": "http://127.0.0.1:53682/callback",
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"code_challenge_method": "S256",
"organization_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"state": "<string>"
}
'import requests
url = "https://iam.basaltic.sh/v1/oauth/authorize"
payload = {
"client_id": "basaltic-cli",
"redirect_uri": "http://127.0.0.1:53682/callback",
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"code_challenge_method": "S256",
"organization_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"state": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
client_id: 'basaltic-cli',
redirect_uri: 'http://127.0.0.1:53682/callback',
code_challenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM',
code_challenge_method: 'S256',
organization_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
state: '<string>'
})
};
fetch('https://iam.basaltic.sh/v1/oauth/authorize', 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/authorize",
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([
'client_id' => 'basaltic-cli',
'redirect_uri' => 'http://127.0.0.1:53682/callback',
'code_challenge' => 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM',
'code_challenge_method' => 'S256',
'organization_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'state' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"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/oauth/authorize"
payload := strings.NewReader("{\n \"client_id\": \"basaltic-cli\",\n \"redirect_uri\": \"http://127.0.0.1:53682/callback\",\n \"code_challenge\": \"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM\",\n \"code_challenge_method\": \"S256\",\n \"organization_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"state\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
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/oauth/authorize")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"client_id\": \"basaltic-cli\",\n \"redirect_uri\": \"http://127.0.0.1:53682/callback\",\n \"code_challenge\": \"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM\",\n \"code_challenge_method\": \"S256\",\n \"organization_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"state\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://iam.basaltic.sh/v1/oauth/authorize")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"client_id\": \"basaltic-cli\",\n \"redirect_uri\": \"http://127.0.0.1:53682/callback\",\n \"code_challenge\": \"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM\",\n \"code_challenge_method\": \"S256\",\n \"organization_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"state\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"redirect_to": "http://127.0.0.1:53682/callback?code=...&state=..."
}{
"error": {
"code": "INVALID_INPUT",
"message": "Invalid request parameters",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Authentication required",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}{
"error": {
"code": "ACCESS_DENIED",
"message": "You don't have permission to perform this action",
"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"
}
}Authorizations
An OAuth 2.0 bearer token, sent as Authorization: Bearer <token>.
This is the recommended way to authenticate.
Get one by exchanging a service account's access key pair at
POST /v1/oauth/token with grant_type=client_credentials. It is the
standard client-credentials grant, so any OAuth-aware library will
obtain and refresh it for you.
curl -s -u "$KEY_ID:$SECRET" -d grant_type=client_credentials \
https://iam.basaltic.sh/v1/oauth/token
Tokens last an hour by default. The same access key pair is separately your AWS SigV4 credential for the S3-compatible object endpoint, which speaks nothing else.
Body
A signed-in user approving a client, from the console's consent page.
The registered client being approved.
"basaltic-cli"
Where to deliver the code. For the CLI this must be a loopback address
with any port — http://127.0.0.1:<port>/... or http://[::1]:<port>/...
(RFC 8252 section 7.3). localhost is refused: it is a name, and
whatever resolves it decides where the code goes.
"http://127.0.0.1:53682/callback"
Base64url SHA-256 of the client's PKCE verifier, without padding.
"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
S256 only. plain is refused rather than merely discouraged: whoever
intercepts the code also saw the challenge, so a plain challenge
protects nothing.
S256 "S256"
Which organization the resulting session is scoped to. The user must be a member of it.
Opaque value echoed back on the redirect, unchanged. The client generated it and compares it on return.
Response
Where to send the browser next. The URL carries the authorization code, so treat it as a credential: it is single use, valid for five minutes, and must not be logged.
Send the browser here. The URL carries the authorization code and the state — treat it as a credential, and do not log it.
"http://127.0.0.1:53682/callback?code=...&state=..."