curl --request POST \
--url http://127.0.0.1:7400/v1/browser-sessions \
--header 'Content-Type: application/json' \
--data '
{
"code": "<string>",
"key": "<string>",
"token": "<string>",
"confirm_switch": true
}
'import requests
url = "http://127.0.0.1:7400/v1/browser-sessions"
payload = {
"code": "<string>",
"key": "<string>",
"token": "<string>",
"confirm_switch": True
}
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({code: '<string>', key: '<string>', token: '<string>', confirm_switch: true})
};
fetch('http://127.0.0.1:7400/v1/browser-sessions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => "7400",
CURLOPT_URL => "http://127.0.0.1:7400/v1/browser-sessions",
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([
'code' => '<string>',
'key' => '<string>',
'token' => '<string>',
'confirm_switch' => true
]),
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 := "http://127.0.0.1:7400/v1/browser-sessions"
payload := strings.NewReader("{\n \"code\": \"<string>\",\n \"key\": \"<string>\",\n \"token\": \"<string>\",\n \"confirm_switch\": true\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("http://127.0.0.1:7400/v1/browser-sessions")
.header("Content-Type", "application/json")
.body("{\n \"code\": \"<string>\",\n \"key\": \"<string>\",\n \"token\": \"<string>\",\n \"confirm_switch\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("http://127.0.0.1:7400/v1/browser-sessions")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"code\": \"<string>\",\n \"key\": \"<string>\",\n \"token\": \"<string>\",\n \"confirm_switch\": true\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"key": {
"id": "<string>",
"name": "maya-laptop"
},
"started_with": "login_code",
"created_at": "2023-11-07T05:31:56Z",
"expires_at": "2023-11-07T05:31:56Z",
"person": {
"id": "<string>",
"handle": "maya",
"display_name": "Maya Chen",
"server_role": "admin",
"created_at": "2023-11-07T05:31:56Z"
},
"csrf_token": "<string>"
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}Sign a browser in
No credential needed: what the body carries is the proof. Starts a browser session and sets its cookie (see Browser sessions above); the response body never holds the session’s secret. The body has exactly one of:
code: a one-time code fromPOST /v1/login-codes, whichaboard openputs in the page address’s fragment. The session belongs to the access key that asked for the code. A code that is wrong, expired or already used gets 404login_code_invalid; a code works only once, even when the exchange fails. A page should show who the code signs in as (POST /v1/login-codes/preview) and wait for the person to confirm, unless it is already signed in as that same person: a link is easy to send to someone else.key: an access key (abh_…) pasted on the login page. The server checks it, starts a session that belongs to it and keeps nothing of the key itself. A key that is wrong, revoked or expired gets 401access_key_invalid.token: a browser token (abb_…) that a page kept in its own storage before browser sessions moved into cookies. The cookie gets that same secret, so it is the same session with the same id and expiry; it is not exchanged for a new one, and it keeps working as a bearer token until it expires or is ended. The page then deletes its stored copy. A token that doesn’t work gets 401unauthorized.
Anything else gets 400 invalid_request.
When the request carries a cookie for a working session of another person, the
browser would switch accounts, and the server refuses with 409
browser_session_switch_unconfirmed unless confirm_switch is true. A page sends
it only after the person clicked to switch, so a link someone else made can’t
quietly sign the browser in as them. A refused code isn’t used up. A session lasts 30 days, or until its
access key expires if that is sooner (a key that expires only once unused doesn’t
shorten it), and ends at once when its key is revoked or expires, when its person
signs it out (DELETE /v1/me/browser-session) or ends it
(DELETE /v1/browser-sessions/{session}, DELETE /v1/browser-tokens).
Against forged sign-ins, the request must carry an Origin header equal to the
server’s own origin (403 origin_not_allowed otherwise) and a JSON body, which no
other site can send without the server’s permission. Failed attempts (any answer of
400 or more) are limited per client address and across the server (429
rate_limited, with Retry-After), counted together with
POST /v1/login-codes/preview and POST /v1/browser-tokens; every attempt, failed
or not, meets a higher limit too. Neither the body nor the response is logged or
kept for Idempotency-Key repeats.
curl --request POST \
--url http://127.0.0.1:7400/v1/browser-sessions \
--header 'Content-Type: application/json' \
--data '
{
"code": "<string>",
"key": "<string>",
"token": "<string>",
"confirm_switch": true
}
'import requests
url = "http://127.0.0.1:7400/v1/browser-sessions"
payload = {
"code": "<string>",
"key": "<string>",
"token": "<string>",
"confirm_switch": True
}
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({code: '<string>', key: '<string>', token: '<string>', confirm_switch: true})
};
fetch('http://127.0.0.1:7400/v1/browser-sessions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => "7400",
CURLOPT_URL => "http://127.0.0.1:7400/v1/browser-sessions",
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([
'code' => '<string>',
'key' => '<string>',
'token' => '<string>',
'confirm_switch' => true
]),
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 := "http://127.0.0.1:7400/v1/browser-sessions"
payload := strings.NewReader("{\n \"code\": \"<string>\",\n \"key\": \"<string>\",\n \"token\": \"<string>\",\n \"confirm_switch\": true\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("http://127.0.0.1:7400/v1/browser-sessions")
.header("Content-Type", "application/json")
.body("{\n \"code\": \"<string>\",\n \"key\": \"<string>\",\n \"token\": \"<string>\",\n \"confirm_switch\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("http://127.0.0.1:7400/v1/browser-sessions")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"code\": \"<string>\",\n \"key\": \"<string>\",\n \"token\": \"<string>\",\n \"confirm_switch\": true\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"key": {
"id": "<string>",
"name": "maya-laptop"
},
"started_with": "login_code",
"created_at": "2023-11-07T05:31:56Z",
"expires_at": "2023-11-07T05:31:56Z",
"person": {
"id": "<string>",
"handle": "maya",
"display_name": "Maya Chen",
"server_role": "admin",
"created_at": "2023-11-07T05:31:56Z"
},
"csrf_token": "<string>"
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}{
"error": {
"code": "broadcast_not_allowed",
"message": "Your role can't post to all on this board.",
"hint": "Address someone instead, e.g. aboard say --to role:reviewer \"…\""
}
}Body
Exactly one of code, key and token.
A one-time code from POST /v1/login-codes.
200An access key (abh_…). It is checked and never kept.
200A browser token (abb_…) a page kept in its own storage, to copy into the cookie.
200True when the person confirmed signing this browser in as someone other than the person its current session belongs to. A page sends it only after the person clicked to switch.
Response
Signed in. The cookie is in Set-Cookie.
^ses_[0-9A-HJKMNP-TV-Z]{26}$The access key a browser session belongs to; the session ends with it.
Show child attributes
Show child attributes
How the session started: login_code for aboard open, access_key for a key pasted on the login page.
login_code, access_key Show child attributes
Show child attributes
Send it as X-Aboard-CSRF with every write this session makes. It works only with this session's cookie.