// An access token as the token endpoint hands it out, and the two requests that
// ask for one: the authorization-code exchange, which every interactive client
// makes, and the client-credentials grant, for a client acting as itself.
///|
/// What the token endpoint answered.
#derive(Debug, Eq)
pub(all) struct Tokens {
access_token : String
token_type : String
expires_in : Int64?
refresh_token : String?
scope : String?
}
///|
/// Read a token response. `access_token` is the only field a client cannot do
/// without; a server that omits `token_type` is taken at its word that the token
/// goes in the `Authorization` header.
pub fn Tokens::parse(json : Json) -> Tokens raise Refused {
let fields = object_of(json)
if fields.get("error") is Some(String(_)) {
raise Refused(message=refused_text(fields, "the token endpoint refused"))
}
{
access_token: required_text(fields, "access_token", "token response"),
token_type: text_field(fields, "token_type").unwrap_or("Bearer"),
expires_in: match fields.get("expires_in") {
Some(Number(value, ..)) => Some(value.to_int64())
_ => None
},
refresh_token: text_field(fields, "refresh_token"),
scope: text_field(fields, "scope"),
}
}
///|
/// When the token stops being good, in seconds since the epoch, measured from
/// `issued`; `None` when it names no lifetime, which a client reads as "until
/// the server says otherwise".
pub fn Tokens::good_until(self : Tokens, issued : Int64) -> Int64? {
match self.expires_in {
Some(lifetime) => Some(issued + lifetime)
None => None
}
}
///|
/// The token a refresh left this client with. A server may rotate the refresh
/// token, so a response that carries one replaces the old; one that carries
/// none leaves the old in place rather than throwing the only key away. The
/// same holds for the scope: what a response does not name is what was asked
/// for, which is what this client already had.
pub fn Tokens::renewed(self : Tokens, fresh : Tokens) -> Tokens {
{
access_token: fresh.access_token,
token_type: fresh.token_type,
expires_in: fresh.expires_in,
refresh_token: match fresh.refresh_token {
Some(token) => Some(token)
None => self.refresh_token
},
scope: match fresh.scope {
Some(scope) => Some(scope)
None => self.scope
},
}
}
///|
/// The body of an authorization-code exchange (RFC 6749 §4.1.3) with the PKCE
/// verifier and the RFC 8707 resource indicator.
pub fn authorization_code_form(
code~ : String,
redirect_uri~ : String,
verifier~ : String,
resource? : String,
scope? : String,
) -> Array[(String, String)] {
let form = [
("grant_type", "authorization_code"),
("code", code),
("redirect_uri", redirect_uri),
("code_verifier", verifier),
]
with_optional(with_optional(form, "resource", resource), "scope", scope)
}
///|
/// The body of a client-credentials request (RFC 6749 §4.4).
pub fn client_credentials_form(
resource? : String,
scope? : String,
) -> Array[(String, String)] {
let form = [("grant_type", "client_credentials")]
with_optional(with_optional(form, "resource", resource), "scope", scope)
}
///|
/// The body of a refresh (RFC 6749 §6).
pub fn refresh_form(
refresh_token~ : String,
resource? : String,
scope? : String,
) -> Array[(String, String)] {
let form = [("grant_type", "refresh_token"), ("refresh_token", refresh_token)]
with_optional(with_optional(form, "resource", resource), "scope", scope)
}
///|
/// `pairs` with one more, or as they were when there is nothing to add.
fn with_optional(
pairs : Array[(String, String)],
name : String,
value : String?,
) -> Array[(String, String)] {
match value {
Some(value) => [..pairs, (name, value)]
None => pairs
}
}
///|
pub extend Tokens with Debug::{to_repr}
///|
pub extend Tokens with Eq::{not_equal, equal}
///|
/// Which grant a client is set up to use: the interactive one, or the one a
/// client makes on its own behalf.
#derive(Debug, Eq)
pub(all) enum Grant {
AuthorizationCode
ClientCredentials
}
///|
pub extend Grant with Debug::{to_repr}
///|
pub extend Grant with Eq::{not_equal, equal}