HTTP Authentication & Cookies
Servants called from a browser can read and set cookies. An httpOnly session
cookie is the usual way to authenticate browser clients: the browser sends it
with every call, and page scripts cannot read it, so an XSS bug cannot steal
it.
Cookies exist on HTTP, HTTP/3 and WebSocket. Native transports (TCP, shared memory, QUIC) have no headers, so pass credentials there as ordinary IDL arguments.
How it works
- The browser calls a method. Generated TypeScript proxies send every HTTP
call with
credentials: 'include', so the browser attaches its cookies. - Inside the servant,
get_cookie("name")reads them. set_cookie(...)queues aSet-Cookieheader, which the server adds to the reply once the method returns.
On a WebSocket, the cookies are read once from the connection's upgrade
request and stay the same for the whole connection. There is no reply envelope
to carry a Set-Cookie, so set_cookie has no effect there. Make the login
call over HTTP instead.
| Transport | Read cookies | Set cookies |
|---|---|---|
| HTTP, HTTP/3 | yes | yes |
| WebSocket | yes (from the upgrade request) | no |
| TCP, shared memory, QUIC | no (nullopt / nil) |
no (ignored) |
Cross-origin calls
A page served by the same server needs no configuration. A page on another
origin, for example https://app.example.com calling https://api.example.com,
must be listed:
builder.with_http(443)
.ssl("cert.pem", "key.pem")
.allow_origins({"https://app.example.com"});
For a listed origin the server replies with that exact origin in
Access-Control-Allow-Origin and sets Access-Control-Allow-Credentials: true,
which browsers require before they send cookies cross-origin. Calls from
unlisted origins get no CORS headers, so the browser blocks them.
C++
#include <nprpc/http_auth.hpp>
std::optional<std::string> nprpc::http::get_cookie(std::string_view name);
void nprpc::http::set_cookie(std::string_view name,
std::string_view value,
const CookieOptions& opts = {});
void nprpc::http::clear_cookie(std::string_view name,
std::string_view path = "/",
std::string_view domain = {});
get_cookie returns std::nullopt when the cookie is absent, outside a
servant method, or on a transport without cookies. clear_cookie expires the
cookie by setting Max-Age=0.
CookieOptions defaults to the safe choices:
| Field | Default | Meaning |
|---|---|---|
http_only |
true |
Hidden from page scripts |
secure |
true |
Sent over HTTPS only |
same_site |
"Strict" |
"Strict", "Lax" or "None" |
max_age |
std::nullopt |
Lifetime in seconds; unset = until the browser closes |
path |
"/" |
URL path scope |
domain |
"" |
Empty = this host only |
Given this IDL:
exception Unauthenticated {}
interface Auth {
void Login(username: string, password: string) raises(Unauthenticated);
void Logout();
}
interface Data {
string GetProfile() raises(Unauthenticated);
}
the servants look like this:
class AuthImpl : public IAuth_Servant {
public:
void Login(nprpc::flat::Span<char> username,
nprpc::flat::Span<char> password) override
{
auto token = authenticate(username, password); // your logic
if (!token)
throw Unauthenticated();
nprpc::http::set_cookie("session_id", *token, {.max_age = 86400});
}
void Logout() override { nprpc::http::clear_cookie("session_id"); }
};
class DataImpl : public IData_Servant {
public:
std::string GetProfile() override
{
auto token = nprpc::http::get_cookie("session_id");
if (!token)
throw Unauthenticated();
return load_profile(*token); // your logic
}
};
Because the methods declare raises(Unauthenticated), the client receives the
error as that exception type in every language.
Swift
import NPRPC
func getCookie(name: String) -> String?
func setCookie(name: String, value: String, options: CookieOptions = CookieOptions())
func clearCookie(name: String, path: String = "/", domain: String = "")
CookieOptions has the same fields and defaults as in C++: httpOnly,
secure, sameSite, maxAge, path and domain.
final class AuthImpl: AuthServant, @unchecked Sendable {
override func login(username: String, password: String) throws {
guard let token = authenticate(username, password) else { // your logic
throw Unauthenticated()
}
setCookie(name: "session_id", value: token,
options: CookieOptions(maxAge: 86400))
}
override func logout() {
clearCookie(name: "session_id")
}
}
final class DataImpl: DataServant, @unchecked Sendable {
override func getProfile() throws -> String {
guard let token = getCookie(name: "session_id") else {
throw Unauthenticated()
}
return loadProfile(token) // your logic
}
}
TypeScript
Nothing to do: generated proxies already send credentials: 'include', and
the browser stores Set-Cookie replies itself.
Security checklist
- Keep
http_onlyandsecureon for session cookies. - Prefer
same_site = "Strict"; use"Lax"only if you need top-level cross-site navigation to carry the cookie."None"requiressecure. - Issue a new token after login or any privilege change.
- Keep
max_ageshort and refresh server-side, rather than using long-lived cookies. - Never log cookie values.
docs/HTTP_AUTH.md