diff --git a/src/at/oauth.gleam b/src/at/oauth.gleam new file mode 100644 index 0000000..37bc62c --- /dev/null +++ b/src/at/oauth.gleam @@ -0,0 +1,851 @@ +// IMPORTS --------------------------------------------------------------------- + +import at/did.{type Did} +import at/internal/decoding +import gleam/bit_array +import gleam/bool +import gleam/dynamic/decode.{type Decoder} +import gleam/float +import gleam/http.{Post} +import gleam/http/request.{type Request} +import gleam/http/response.{type Response} +import gleam/json +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/result +import gleam/set.{type Set} +import gleam/string +import gleam/time/timestamp +import gleam/uri.{type Uri, Uri} +import munch +import ywt/verify_key.{type VerifyKey} + +// This module is roughly structured to follow the typical authentication flow: +// +// 1. Resource and authorisation server discovery +// 2. Client registration +// 3. Pushed authorisation requests +// 4. Authorisation grants +// 5. Session creation and refresh +// 6. DPoP request signing and nonce threading +// +// Good luck 🫡 + +// OAUTH SERVERS --------------------------------------------------------------- + +/// +/// +pub type ResourceServer { + ResourceServer(authorisation_server: Uri) +} + +/// +/// +pub opaque type AuthorisationServer { + AuthorisationServer( + issuer: Uri, + authorisation_endpoint: Uri, + token_endpoint: Uri, + response_types_supported: Set(String), + grant_types_supported: Set(String), + scopes_supported: Set(String), + pushed_authorization_request_endpoint: Uri, + ) +} + +/// +/// +pub fn query_resource_server(url: Uri) -> Result(Request(String), Nil) { + use request <- result.try(request.from_uri(url)) + + request + |> request.set_path("/.well-known/oauth-protected-resource") + |> request.set_header("accept", "application/json") + |> Ok +} + +/// +/// +pub fn resource_server_decoder() -> Decoder(ResourceServer) { + use authorisation_server <- decode.field("authorization_servers", { + decode.then(decode.list(decode.string), fn(servers) { + case servers { + [server] -> + case uri.parse(server) { + Ok(uri) -> decode.success(uri) + Error(_) -> decode.failure(uri.empty, "uri.Uri") + } + + [] | [_, ..] -> + decode.failure(uri.empty, "[\"\"]") + } + }) + }) + + decode.success(ResourceServer(authorisation_server:)) +} + +/// +/// +pub fn query_authorisation_server(url: Uri) -> Result(Request(String), Nil) { + use request <- result.try(request.from_uri(url)) + + request + |> request.set_path("/.well-known/oauth-authorization-server") + |> request.set_header("accept", "application/json") + |> Ok +} + +/// +/// +pub fn authorisation_server_decoder( + origin: Uri, +) -> Decoder(AuthorisationServer) { + use issuer <- decode.field("issuer", { + decode.then(decoding.uri(), fn(issuer) { + case issuer == origin { + True -> decode.success(issuer) + False -> decode.failure(issuer, uri.to_string(origin)) + } + }) + }) + + use authorisation_endpoint <- decode.field("authorization_endpoint", { + decoding.uri() + }) + + use token_endpoint <- decode.field("token_endpoint", decoding.uri()) + use response_types_supported <- decode.field("response_types_supported", { + decode.then(decoding.set(decode.string), fn(set) { + case set.contains(set, "code") { + True -> decode.success(set) + False -> decode.failure(set, "[\"code\", ..]") + } + }) + }) + + use grant_types_supported <- decode.field("grant_types_supported", { + decode.then(decoding.set(decode.string), fn(set) { + use <- bool.guard(!set.contains(set, "authorization_code"), { + decode.failure(set, "[\"authorization_code\", ..]") + }) + + use <- bool.guard(!set.contains(set, "refresh_token"), { + decode.failure(set, "[\"refresh_token\", ..]") + }) + + decode.success(set) + }) + }) + + use _ <- decode.field("code_challenge_methods_supported", { + decode.then(decoding.set(decode.string), fn(set) { + case set.contains(set, "S256") { + True -> decode.success(set) + False -> decode.failure(set, "[\"S256\", ..]") + } + }) + }) + + use _ <- decode.field("token_endpoint_auth_methods_supported", { + decode.then(decoding.set(decode.string), fn(set) { + use <- bool.guard(!set.contains(set, "none"), { + decode.failure(set, "[\"none\", ..]") + }) + + use <- bool.guard(!set.contains(set, "private_key_jwt"), { + decode.failure(set, "[\"private_key_jwt\", ..]") + }) + + decode.success(set) + }) + }) + + use _ <- decode.field("token_endpoint_auth_signing_alg_values_supported", { + decode.then(decoding.set(decode.string), fn(set) { + use <- bool.guard(set.contains(set, "none"), { + decode.failure(set, "[..]") + }) + + use <- bool.guard(!set.contains(set, "ES256"), { + decode.failure(set, "[\"ES256\", ..]") + }) + + decode.success(set) + }) + }) + + use scopes_supported <- decode.field("scopes_supported", { + decode.then(decoding.set(decode.string), fn(set) { + case set.contains(set, "atproto") { + True -> decode.success(set) + False -> decode.failure(set, "[\"atproto\", ..]") + } + }) + }) + + use _ <- decode.field("authorization_response_iss_parameter_supported", { + decode.then(decode.bool, fn(bool) { + case bool { + True -> decode.success(bool) + False -> decode.failure(bool, "True") + } + }) + }) + + use _ <- decode.field("require_pushed_authorization_requests", { + decode.then(decode.bool, fn(bool) { + case bool { + True -> decode.success(bool) + False -> decode.failure(bool, "True") + } + }) + }) + + use pushed_authorization_request_endpoint <- decode.field( + "pushed_authorization_request_endpoint", + decoding.uri(), + ) + + use _ <- decode.field("dpop_signing_alg_values_supported", { + decode.then(decoding.set(decode.string), fn(set) { + case set.contains(set, "ES256") { + True -> decode.success(set) + False -> decode.failure(set, "[\"ES256\", ..]") + } + }) + }) + + use _ <- decoding.optional_field("require_request_uri_registration", { + decode.then(decode.bool, fn(bool) { + case bool { + True -> decode.success(bool) + False -> decode.failure(bool, "True") + } + }) + }) + + use _ <- decode.field("client_id_metadata_document_supported", { + decode.then(decode.bool, fn(bool) { + case bool { + True -> decode.success(bool) + False -> decode.failure(bool, "True") + } + }) + }) + + decode.success(AuthorisationServer( + issuer:, + authorisation_endpoint:, + token_endpoint:, + response_types_supported:, + grant_types_supported:, + scopes_supported:, + pushed_authorization_request_endpoint:, + )) +} + +/// +/// +pub fn supported_grant_types(server: AuthorisationServer) -> Set(String) { + server.grant_types_supported +} + +/// +/// +pub fn supported_scopes(server: AuthorisationServer) -> Set(String) { + server.scopes_supported +} + +// OAUTH CLIENT ---------------------------------------------------------------- + +/// +/// +pub type Client { + Client( + id: Uri, + name: Option(String), + application_type: ApplicationType, + scopes: Set(String), + response_types: Set(String), + redirect_uri: Uri, + // + homepage: Option(Uri), + logo: Option(Uri), + terms_of_service: Option(Uri), + privacy_policy: Option(Uri), + ) +} + +/// +/// +pub type ApplicationType { + Web + Native +} + +/// +/// +pub fn public_client( + id: Uri, + redirect_uri: Uri, + application_type: ApplicationType, +) -> Client { + let id = case id.host { + Some("localhost") -> Uri(..id, scheme: Some("http"), query: None) + _ -> id + } + + Client( + id: id, + name: None, + application_type:, + scopes: set.from_list(["atproto"]), + response_types: set.from_list(["code"]), + redirect_uri:, + homepage: None, + logo: None, + terms_of_service: None, + privacy_policy: None, + ) +} + +/// +/// +pub fn localhost_client(redirect_port: Int, redirect_path: String) -> Client { + let id = Uri(..uri.empty, scheme: Some("http"), host: Some("localhost")) + let redirect_uri = + Uri( + ..uri.empty, + scheme: Some("http"), + host: Some("127.0.0.1"), + port: Some(redirect_port), + path: redirect_path, + ) + + public_client(id, redirect_uri, Native) +} + +/// +/// +pub fn client_name(client: Client, name: String) -> Client { + Client(..client, name: Some(name)) +} + +/// +/// +pub fn client_scopes(client: Client, scopes: List(String)) -> Client { + Client(..client, scopes: list.fold(scopes, client.scopes, set.insert)) +} + +/// +/// +pub fn client_response_types( + client: Client, + response_types: List(String), +) -> Client { + Client(..client, response_types: { + list.fold(response_types, client.response_types, set.insert) + }) +} + +/// +/// +pub fn client_homepage(client: Client, homepage: Uri) -> Client { + Client(..client, homepage: Some(homepage)) +} + +/// +/// +pub fn client_logo(client: Client, logo: Uri) -> Client { + Client(..client, logo: Some(logo)) +} + +/// +/// +pub fn client_terms_of_service( + client: Client, + terms_of_service: Uri, +) -> Client { + Client(..client, terms_of_service: Some(terms_of_service)) +} + +/// +/// +pub fn client_privacy_policy(client: Client, privacy_policy: Uri) -> Client { + Client(..client, privacy_policy: Some(privacy_policy)) +} + +fn client_to_id(client: Client) -> Uri { + case client.id.host, client.id.query { + Some("localhost"), None -> { + let scope = set.to_list(client.scopes) |> string.join(" ") + let query = Some("scope=" <> uri.percent_encode(scope)) + + Uri(..client.id, query:) + } + + Some("localhost"), Some(query) -> { + let scope = set.to_list(client.scopes) |> string.join(" ") + let query = Some(query <> "&scope=" <> uri.percent_encode(scope)) + + Uri(..client.id, query:) + } + + _, _ -> client.id + } +} + +// PUSHED AUTHORISATION REQUESTS ----------------------------------------------- + +/// +/// +pub opaque type AuthorisationToken { + AuthorisationToken(request_uri: String, expires_in: Int) +} + +/// +/// +pub opaque type AuthorisationGrant(verified) { + AuthorisationGrant(code: String, state: AuthorisationState, issuer: Uri) +} + +/// +/// +pub opaque type PkceCodeVerifier { + PkceCodeVerifier(verifier: String, challenge: String) +} + +/// +/// +pub opaque type AuthorisationState { + AuthorisationState(value: String) +} + +/// +/// +pub fn request_authorisation( + client: Client, + server: AuthorisationServer, + login_hint: Option(String), + dpop_verify_key: VerifyKey, + dpop_nonce: Option(Nonce), +) -> #(DpopRequest(String), PkceCodeVerifier, AuthorisationState) { + let assert Ok(request) = + request.from_uri(server.pushed_authorization_request_endpoint) + + let code_verifier_bytes = strong_random_bytes(64) + let code_verifier = + code_verifier_bytes + |> bit_array.base64_url_encode(False) + let code_challenge = + <> + |> munch.hash_bits(munch.sha256, _) + |> bit_array.base64_url_encode(False) + + let state = strong_random_bytes(32) |> bit_array.base64_url_encode(False) + let scope = set.to_list(client.scopes) |> string.join(" ") + + let body = [ + #("client_id", uri.to_string(client_to_id(client))), + #("response_type", "code"), + #("code_challenge", code_challenge), + #("code_challenge_method", "S256"), + #("state", state), + #("redirect_uri", uri.to_string(client.redirect_uri)), + #("scope", scope), + ] + + let body = case login_hint { + Some(login_hint) -> [#("login_hint", login_hint), ..body] + None -> body + } + + let request = + request + |> request.set_method(Post) + |> request.set_header("content-type", "application/x-www-form-urlencoded") + |> request.set_body(uri.query_to_string(body)) + + #( + dpop_request(request, dpop_verify_key, dpop_nonce, None), + PkceCodeVerifier(verifier: code_verifier, challenge: code_challenge), + AuthorisationState(value: state), + ) +} + +/// +/// +pub fn authorisation_token_decoder() -> Decoder(AuthorisationToken) { + use request_uri <- decode.field("request_uri", decode.string) + use expires_in <- decode.field("expires_in", decode.int) + + decode.success(AuthorisationToken(request_uri:, expires_in:)) +} + +/// +/// +pub fn redirect( + client: Client, + server: AuthorisationServer, + token: AuthorisationToken, +) -> Uri { + let query = + uri.query_to_string([ + #("client_id", uri.to_string(client_to_id(client))), + #("request_uri", token.request_uri), + ]) + + Uri(..server.authorisation_endpoint, query: Some(query)) +} + +/// +/// +pub fn handle_authorisation_callback( + request: Request(_), + client: Client, +) -> Result(AuthorisationGrant(Unverified), Nil) { + use <- bool.guard(request.method != http.Get, Error(Nil)) + use <- bool.guard(Some(request.host) != client.redirect_uri.host, Error(Nil)) + use <- bool.guard(request.path != client.redirect_uri.path, Error(Nil)) + + use _ <- result.try(case client.redirect_uri.host { + Some("127.0.0.1") | Some("[::1]") -> Ok(Nil) + Some(_) -> { + use <- bool.guard(request.scheme != http.Https, Error(Nil)) + use <- bool.guard(request.port != client.redirect_uri.port, Error(Nil)) + + Ok(Nil) + } + + None -> Error(Nil) + }) + + use parameters <- result.try( + request.query + |> option.unwrap("") + |> uri.parse_query, + ) + + use state <- result.try(list.key_find(parameters, "state")) + use issuer <- result.try(list.key_find(parameters, "iss")) + use code <- result.try(list.key_find(parameters, "code")) + use issuer <- result.try(uri.parse(issuer)) + + Ok(AuthorisationGrant(code:, state: AuthorisationState(value: state), issuer:)) +} + +// AUTH SESSIONS & TOKENS ------------------------------------------------------ + +pub type Session(verified) { + Session( + access_token: AccessToken, + refresh_token: RefreshToken, + expires_in: Option(Int), + scopes: Set(String), + subject: Did, + ) +} + +pub opaque type AccessToken { + AccessToken(value: String) +} + +pub opaque type RefreshToken { + RefreshToken(value: String) +} + +/// +/// +pub fn create_session( + client: Client, + server: AuthorisationServer, + authorisation_grant: AuthorisationGrant(Verified), + pkce_verifier: PkceCodeVerifier, + dpop_verify_key: VerifyKey, + dpop_nonce: Nonce, +) -> DpopRequest(String) { + let assert Ok(request) = request.from_uri(server.token_endpoint) + + let body = [ + #("client_id", uri.to_string(client_to_id(client))), + #("grant_type", "authorization_code"), + #("code", authorisation_grant.code), + #("code_verifier", pkce_verifier.verifier), + #("redirect_uri", uri.to_string(client.redirect_uri)), + ] + + let request = + request + |> request.set_method(Post) + |> request.set_header("content-type", "application/x-www-form-urlencoded") + |> request.set_body(uri.query_to_string(body)) + + dpop_request(request, dpop_verify_key, Some(dpop_nonce), None) +} + +/// +/// +pub fn refresh_session( + client: Client, + server: AuthorisationServer, + session: Session(Verified), + dpop_verify_key: VerifyKey, + dpop_nonce: Nonce, +) -> DpopRequest(String) { + let assert Ok(request) = request.from_uri(server.token_endpoint) + + let body = [ + #("client_id", uri.to_string(client_to_id(client))), + #("grant_type", "refresh_token"), + #("refresh_token", session.refresh_token.value), + ] + + let request = + request + |> request.set_method(Post) + |> request.set_header("content-type", "application/x-www-form-urlencoded") + |> request.set_body(uri.query_to_string(body)) + + dpop_request(request, dpop_verify_key, Some(dpop_nonce), Some(session)) +} + +pub fn session_decoder() -> Decoder(Session(Unverified)) { + use access_token <- decode.field("access_token", decode.string) + use _ <- decode.field("token_type", { + decode.then(decode.string, fn(token_type) { + case token_type { + "DPoP" -> decode.success(token_type) + _ -> decode.failure(token_type, "DPoP") + } + }) + }) + + use refresh_token <- decode.field("refresh_token", decode.string) + use expires_in <- decoding.optional_field("expires_in", decode.int) + use scopes <- decode.field("scope", { + decode.then(decode.string, fn(scope) { + let scopes = + scope + |> string.split(" ") + |> list.map(string.trim) + |> set.from_list + |> set.delete("") + + case set.contains(scopes, "atproto") { + True -> decode.success(scopes) + False -> decode.failure(scopes, "[\"atproto\", ..]") + } + }) + }) + + use subject <- decode.field("sub", did.decoder()) + + decode.success(Session( + access_token: AccessToken(value: access_token), + refresh_token: RefreshToken(value: refresh_token), + expires_in:, + scopes: scopes, + subject:, + )) +} + +// DPOP REQUESTS --------------------------------------------------------------- + +pub opaque type DpopRequest(body) { + DpopRequest(raw: Request(body), verify_key: VerifyKey, proof: String) +} + +pub opaque type Nonce { + Nonce(value: String) +} + +pub type DpopResponse { + Continue(Nonce) + Retry(Nonce) +} + +@internal +pub fn dpop_request( + request: Request(body), + verify_key: VerifyKey, + nonce: Option(Nonce), + session: Option(Session(Verified)), +) -> DpopRequest(body) { + let header = + json.object([ + #("typ", json.string("dpop+jwt")), + #("alg", json.string("ES256")), + #("jwk", verify_key.to_jwk(verify_key)), + ]) + + let jti = strong_random_bytes(64) |> bit_array.base64_url_encode(False) + let iat = + timestamp.system_time() + |> timestamp.to_unix_seconds + |> float.round + + let payload = [ + #("jti", json.string(jti)), + #("htm", json.string(request.method |> http.method_to_string)), + #("htu", json.string(request.to_uri(request) |> uri.to_string)), + #("iat", json.int(iat)), + ] + + let payload = case session { + Some(Session(access_token:, ..)) -> { + let ath = + <> + |> munch.hash_bits(munch.sha256, _) + |> bit_array.base64_url_encode(False) + + [#("ath", json.string(ath)), ..payload] + } + + None -> payload + } + + let payload = case nonce { + Some(nonce) -> + json.object([#("nonce", json.string(nonce.value)), ..payload]) + + None -> json.object(payload) + } + + let proof = + bit_array.base64_url_encode(<>, False) + <> "." + <> bit_array.base64_url_encode(<>, False) + + let request = case session { + Some(Session(access_token:, ..)) -> + request.set_header( + request, + "authorization", + "DPoP " <> access_token.value, + ) + None -> request + } + + DpopRequest(raw: request, verify_key:, proof:) +} + +/// +/// +pub fn proof(dpop_request: DpopRequest(body)) -> String { + dpop_request.proof +} + +/// +/// +pub fn prepare(request: DpopRequest(body), signature: String) -> Request(body) { + request.raw + |> request.set_header("dpop", request.proof <> "." <> signature) +} + +/// +/// +pub fn retry( + builder: DpopRequest(body), + nonce: Nonce, + session: Option(Session(Verified)), +) -> DpopRequest(body) { + dpop_request(builder.raw, builder.verify_key, Some(nonce), session) +} + +/// +/// +pub fn extract_nonce(response: Response(String)) -> Result(DpopResponse, Nil) { + case response.get_header(response, "dpop-nonce") { + // The spec says the server should return a 401 but naturally the bluesky + // pds returns a 400 just to be awkward. + Ok(nonce) if response.status == 400 || response.status == 401 -> + // Resource servers will return the error in the body as a JSON object with + // the error code `use_dpop_nonce` ... + case json.parse(response.body, decode.at(["error"], decode.string)) { + Ok("use_dpop_nonce") -> Ok(Retry(Nonce(nonce))) + Ok(_) | Error(_) -> + // ... but authorisation servers will return the error in a + // `www-authenticate` header instead. + case response.get_header(response, "www-authenticate") { + Ok("DPoP error=\"use_dpop_nonce\"" <> _) -> Ok(Retry(Nonce(nonce))) + Ok(_) | Error(_) -> Ok(Continue(Nonce(nonce))) + } + } + + Ok(nonce) -> Ok(Continue(Nonce(nonce))) + Error(_) -> Error(Nil) + } +} + +// VERIFICATION ---------------------------------------------------------------- + +/// +/// +pub type Verified + +/// +/// +pub type Unverified + +/// +/// +pub fn verify_authorisation_grant( + grant: AuthorisationGrant(Unverified), + server: AuthorisationServer, + state: AuthorisationState, +) -> Result(AuthorisationGrant(Verified), Nil) { + use <- bool.guard(grant.state != state, Error(Nil)) + use <- bool.guard(grant.issuer != server.issuer, Error(Nil)) + + Ok(AuthorisationGrant( + code: grant.code, + state: grant.state, + issuer: grant.issuer, + )) +} + +/// +/// +pub fn verify_session_subject( + session: Session(Unverified), + subject: Did, +) -> Result(Session(Verified), Nil) { + case session.subject == subject { + True -> + Ok(Session( + access_token: session.access_token, + refresh_token: session.refresh_token, + expires_in: session.expires_in, + scopes: session.scopes, + subject: session.subject, + )) + False -> Error(Nil) + } +} + +/// +/// +pub fn verify_session_issuer( + session: Session(Unverified), + issuer: AuthorisationServer, + resolved: Uri, +) -> Result(Session(Verified), Nil) { + case issuer.issuer == resolved { + True -> + Ok(Session( + access_token: session.access_token, + refresh_token: session.refresh_token, + expires_in: session.expires_in, + scopes: session.scopes, + subject: session.subject, + )) + False -> Error(Nil) + } +} + +// UTILS ----------------------------------------------------------------------- + +@external(erlang, "crypto", "strong_rand_bytes") +@external(javascript, "./oauth_ffi.mjs", "strongRandomBytes") +fn strong_random_bytes(length: Int) -> BitArray diff --git a/src/at/oauth_ffi.mjs b/src/at/oauth_ffi.mjs new file mode 100644 index 0000000..2ffd38d --- /dev/null +++ b/src/at/oauth_ffi.mjs @@ -0,0 +1,9 @@ +import { BitArray$BitArray } from "../gleam.mjs"; + +export function strongRandomBytes(sizee) { + const array = new Uint8Array(sizee); + + webCrypto().getRandomValues(array); + + return BitArray$BitArray(array); +} diff --git a/src/at/xrpc.gleam b/src/at/xrpc.gleam index e982f4e..d832936 100644 --- a/src/at/xrpc.gleam +++ b/src/at/xrpc.gleam @@ -1,20 +1,36 @@ // IMPORTS --------------------------------------------------------------------- import at/nsid.{type Nsid} +import at/oauth.{type Nonce, type Session, type Verified} import gleam/http.{Get, Https, Post} import gleam/http/request.{type Request, Request} import gleam/json.{type Json} import gleam/option.{type Option, None, Some} import gleam/uri.{type Uri} +import ywt/sign_key.{type SignKey} +import ywt/verify_key.{type VerifyKey} // TYPES ----------------------------------------------------------------------- /// /// -pub opaque type Client { - Client(host: String, port: Option(Int)) +pub opaque type Client(authenticated) { + Client( + host: String, + port: Option(Int), + keys: Option(#(SignKey, VerifyKey)), + session: Option(Session(Verified)), + ) } +/// +/// +pub type Authenticated + +/// +/// +pub type Public + /// /// pub type Error { @@ -25,17 +41,59 @@ pub type Error { /// /// -pub fn new(uri: Uri) -> Result(Client, Nil) { +pub fn new(uri: Uri) -> Result(Client(Public), Nil) { case uri.host { + Some(host) -> Ok(Client(host:, port: uri.port, keys: None, session: None)) None -> Error(Nil) - Some(host) -> Ok(Client(host:, port: uri.port)) } } +// QUERIES --------------------------------------------------------------------- + +/// +/// +pub fn sign_key(client: Client(Authenticated)) -> SignKey { + let assert Some(keys) = client.keys + + keys.0 +} + +/// +/// +pub fn verify_key(client: Client(Authenticated)) -> VerifyKey { + let assert Some(keys) = client.keys + + keys.1 +} + +// MANIPULATIONS --------------------------------------------------------------- + +/// +/// +pub fn authenticate( + client: Client(Public), + sign_key: SignKey, + verify_key: VerifyKey, + session: Session(Verified), +) -> Client(Authenticated) { + Client(..client, keys: Some(#(sign_key, verify_key)), session: Some(session)) +} + +/// +/// +pub fn refresh( + client: Client(Authenticated), + session: Session(Verified), +) -> Client(Authenticated) { + Client(..client, session: Some(session)) +} + +// REQUESTS -------------------------------------------------------------------- + /// /// pub fn query( - client: Client, + client: Client(_), path: Nsid, parameters: List(#(String, String)), ) -> Request(String) { @@ -54,10 +112,24 @@ pub fn query( ) } +/// +/// +pub fn authenticated_query( + client: Client(Authenticated), + nonce: Nonce, + path: Nsid, + parameters: List(#(String, String)), +) -> oauth.DpopRequest(String) { + let assert Some(keys) = client.keys + + query(client, path, parameters) + |> oauth.dpop_request(keys.1, Some(nonce), client.session) +} + /// /// pub fn procedure( - client: Client, + client: Client(_), path: Nsid, parameters: List(#(String, String)), body: Json, @@ -78,3 +150,18 @@ pub fn procedure( }, ) } + +/// +/// +pub fn authenticated_procedure( + client: Client(Authenticated), + nonce: Nonce, + path: Nsid, + parameters: List(#(String, String)), + body: Json, +) -> oauth.DpopRequest(String) { + let assert Some(keys) = client.keys + + procedure(client, path, parameters, body) + |> oauth.dpop_request(keys.1, Some(nonce), client.session) +}