// The bearer token an HTTP transport checks before it serves anything.
//
// A token arrives as `Authorization: Bearer …`; what it says is only worth
// reading once its signature has been checked, which is the order here: the
// token is verified first, and only a token that verified is asked who the
// caller is.
///|
/// The caller a token vouched for: its `sub`, and every claim as it arrived.
///
/// A claim is whatever the issuer put there — a scope list, a tenant, a role.
/// Nothing here interprets them; a handler reads what it needs.
pub(all) struct Principal {
subject : String?
claims : Map[String, Json]
} derive(Debug, Eq)
///|
pub extend Principal with Debug::{to_repr}
///|
pub extend Principal with Eq::{not_equal, equal}
///|
/// Whether the token grants `scope`, read from the `scope` claim (space
/// separated) or `scp` (a list), which are the two spellings in use.
pub fn Principal::has_scope(self : Principal, scope : String) -> Bool {
let from_words = match self.claims.get("scope") {
Some(String(scopes)) => scopes.split(" ").contains(scope)
_ => false
}
if from_words {
return true
}
match self.claims.get("scp") {
Some(Array(scopes)) =>
scopes.any(entry => entry is String(text) && text == scope)
_ => false
}
}
///|
/// How a transport checks a bearer token (← the authorization section of the
/// protocol): a request without one is answered `401` with a
/// `WWW-Authenticate` challenge, and one whose token does not verify is
/// answered the same way with `invalid_token`.
///
/// Two ways to build one: `Bearer::hmac` for a token signed with a shared
/// secret, which is what a test and a single-tenant deployment use, and
/// `Bearer::own` for anything else — an RSA or ECDSA key, a JSON Web Key Set
/// fetched from the issuer, an opaque token looked up in a table. `own` takes
/// the whole decision, so this library never has an opinion about how a token
/// is judged, only about what happens when it is not.
pub struct Bearer {
priv check : (String) -> Result[Principal, String]
}
///|
/// Check tokens with `check`, which returns the caller or the reason the token
/// is refused. The reason travels to the client in `error_description`, so it
/// should say what is wrong without saying anything a stranger could use.
pub fn Bearer::own(check~ : (String) -> Result[Principal, String]) -> Bearer {
{ check, }
}
///|
/// Check tokens signed with `secret` by a shared key: `alg` is the algorithm
/// the verifier uses, and a token that names another one is refused rather
/// than trusted.
///
/// `issuer` and `audience` are checked when given, so a token minted for
/// another service does not open this one. `clock` is the instant in seconds
/// since the epoch that expiry is measured against, which a test fixes and a
/// deployment leaves alone.
pub fn Bearer::hmac(
secret~ : String,
alg~ : @jwt.Alg = HS256,
issuer? : String,
audience~ : Array[String] = [],
leeway? : Int64,
clock~ : () -> Int64 = fn() { wall_clock() },
) -> Bearer {
let key = @jwt.mac(fn() {
@hmac.Hmac::new(@utf8.encode(secret)[:], fn() { @sha2.Hasher::new() })
})
Bearer::own(check=fn(token) {
let claims = @jwt.verify(
token,
alg,
key,
now=@jwt.At(clock()),
issuer?,
audience~,
leeway?,
) catch {
e => return Err(refusal_text(e))
}
let subject = match claims.get("sub") {
Some(String(subject)) => Some(subject)
_ => None
}
Ok({ subject, claims, })
})
}
///|
/// Check the `Authorization` header of a request. `Err` carries the reason the
/// token is refused, phrased for the client.
pub fn Bearer::check(
self : Bearer,
authorization : String,
) -> Result[Principal, String] {
let scheme = match authorization.find(" ") {
Some(at) => authorization.exact_view(end=at).to_owned()
None => return Err("the Authorization header carries no token")
}
if !scheme.equal_ignore_ascii_case("bearer") {
return Err("the Authorization header is not a bearer token")
}
let token = authorization
.exact_view(start=scheme.length() + 1)
.to_owned()
.trim(chars=" ")
.to_owned()
if token.is_empty() {
return Err("the Authorization header carries no token")
}
(self.check)(token)
}
///|
/// Why a token did not verify, in words a client may read: the constructor
/// says what went wrong, and nothing else of the token appears.
fn refusal_text(e : @jwt.Refused) -> String {
match e {
Signature => "the token's signature does not verify"
Expired(_) => "the token has expired"
TooEarly(_) => "the token is not valid yet"
Algorithm(want~, got~) =>
"the token is signed with \{got}, this server expects \{want}"
Issuer(want~, got~) =>
"the token is issued by \{got}, this server expects \{want}"
Audience(_) => "the token is not for this audience"
Subject(want~, got~) =>
"the token is for \{got}, this server expects \{want}"
IssuedAt(_) => "the token was issued in the future"
Type(want~, got~) =>
"the token is of type \{got}, this server expects \{want}"
Missing(claim~) => "the token has no \{claim} claim"
_ => "the token is malformed"
}
}
///|
/// Seconds since the epoch, from the runtime's clock.
fn wall_clock() -> Int64 {
@async.now() / 1_000_000_000L
}