// What the client registers and what the server answers, plus how the token
// endpoint is asked to authenticate the client (RFC 7591, RFC 6749 §2.3.1 and
// the URL-based client identifiers of MCP's client metadata documents).
///|
/// What a client says about itself: at registration, and in the document a
/// URL client identifier points at.
#derive(Debug, Eq)
pub(all) struct Metadata {
client_name : String
redirect_uris : Array[String]
grant_types : Array[String]
response_types : Array[String]
token_endpoint_auth_method : String
scope : String?
}
///|
/// A client's metadata. `refresh_token` is among the grants because a client
/// that can keep a session wants one; `none` is the authentication method of a
/// public client that a loopback redirect makes safe with PKCE.
pub fn Metadata::new(
client_name~ : String,
redirect_uris~ : Array[String],
scope? : String,
grant_types~ : Array[String] = ["authorization_code", "refresh_token"],
response_types~ : Array[String] = ["code"],
token_endpoint_auth_method~ : String = "none",
) -> Metadata {
{
client_name,
redirect_uris,
grant_types,
response_types,
token_endpoint_auth_method,
scope,
}
}
///|
/// The body of a registration request.
pub fn Metadata::body(self : Metadata) -> Map[String, Json] {
let out : Map[String, Json] = {
"client_name": Json::string(self.client_name),
"redirect_uris": Json::array(self.redirect_uris.map(Json::string)),
"grant_types": Json::array(self.grant_types.map(Json::string)),
"response_types": Json::array(self.response_types.map(Json::string)),
"token_endpoint_auth_method": Json::string(self.token_endpoint_auth_method),
}
if self.scope is Some(scope) {
out["scope"] = Json::string(scope)
}
out
}
///|
/// What a client is known by: the identifier the server issued, or the one a
/// deployment was given, and the secret if it has one.
#derive(Debug, Eq)
pub(all) struct Registration {
client_id : String
client_secret : String?
token_endpoint_auth_method : String?
}
///|
/// Read a registration response.
pub fn Registration::parse(json : Json) -> Registration raise Refused {
let fields = object_of(json)
{
client_id: required_text(fields, "client_id", "registration response"),
client_secret: text_field(fields, "client_secret"),
token_endpoint_auth_method: text_field(fields, "token_endpoint_auth_method"),
}
}
///|
/// How the token endpoint is asked to authenticate the client.
#derive(Debug, Eq)
pub(all) enum Credentials {
Basic
Post
Public
}
///|
/// The method to use, given what the server says it supports and how the client
/// was registered. Priority is the one a client that holds a secret should
/// follow: what registration returned when the server lists it, else HTTP
/// Basic, else the secret in the body, else nothing.
pub fn Credentials::select(
registration : Registration,
supported : Array[String],
) -> Credentials {
let registered = registration.token_endpoint_auth_method
if registered is Some(choice) {
let usable = supported.is_empty() || supported.contains(choice)
if usable {
match choice {
"client_secret_basic" => return Basic
"client_secret_post" => return Post
"none" => return Public
_ => ()
}
}
}
let has_secret = registration.client_secret is Some(_)
if has_secret &&
(supported.is_empty() || supported.contains("client_secret_basic")) {
return Basic
}
if has_secret && supported.contains("client_secret_post") {
return Post
}
if supported.contains("none") || !has_secret {
return Public
}
Post
}
///|
/// The `Authorization` header value for this method, when it uses one. Basic
/// credentials are `client_id:client_secret` in base64, both percent-encoded
/// as RFC 6749 §2.3.1 requires.
pub fn Credentials::authorization(
self : Credentials,
registration : Registration,
) -> String? {
match self {
Basic =>
match registration.client_secret {
Some(secret) => {
let pair = "\{percent_encode(registration.client_id)}:\{percent_encode(secret)}"
Some(
"Basic " +
@base64.encode(@utf8.encode(pair)[:], kind=Std, padding=true),
)
}
None => None
}
_ => None
}
}
///|
/// The fields this method puts in the token request's body.
pub fn Credentials::form(
self : Credentials,
registration : Registration,
) -> Array[(String, String)] {
match self {
Basic => []
Post =>
[
("client_id", registration.client_id),
..match registration.client_secret {
Some(secret) => [("client_secret", secret)]
None => []
},
]
Public => [("client_id", registration.client_id)]
}
}
///|
/// Percent-encode everything but the unreserved characters, which is what a
/// form body and an OAuth parameter take.
pub fn percent_encode(text : StringView) -> String {
let out = StringBuilder::StringBuilder()
for byte in @utf8.encode(text) {
let code = byte.to_int()
let unreserved = (code >= 0x41 && code <= 0x5A) ||
(code >= 0x61 && code <= 0x7A) ||
(code >= 0x30 && code <= 0x39) ||
code == 0x2D ||
code == 0x2E ||
code == 0x5F ||
code == 0x7E
if unreserved {
out.write_char(code.unsafe_to_char())
} else {
out.write_char('%')
out.write_char(hex_digit(code / 16))
out.write_char(hex_digit(code % 16))
}
}
out.to_string()
}
///|
/// One uppercase hexadecimal digit.
fn hex_digit(value : Int32) -> Char {
if value < 10 {
(0x30 + value).unsafe_to_char()
} else {
(0x41 + value - 10).unsafe_to_char()
}
}
///|
/// `pairs` as an `application/x-www-form-urlencoded` body.
pub fn form_encode(pairs : Array[(String, String)]) -> String {
pairs
.map(pair => "\{percent_encode(pair.0)}=\{percent_encode(pair.1)}")
.join("&")
}
///|
pub extend Metadata with Debug::{to_repr}
///|
pub extend Metadata with Eq::{not_equal, equal}
///|
pub extend Registration with Debug::{to_repr}
///|
pub extend Registration with Eq::{not_equal, equal}
///|
pub extend Credentials with Debug::{to_repr}
///|
pub extend Credentials with Eq::{not_equal, equal}