Current section
Files
Jump to
Current section
Files
src/atproto/oauth/core/metadata.gleam
//// atproto OAuth discovery as `Effect`s: from a user's PDS, find the
//// authorization server and its endpoints. Two hops of well-known metadata:
//// PDS /.well-known/oauth-protected-resource -> authorization_servers[]
//// issuer /.well-known/oauth-authorization-server -> endpoints
//// The `AuthServerMetadata` type and both decoders live here as the single
//// shared copy; the per-target wrappers alias the type and run the effects.
import atproto/oauth/core/effect.{type Effect}
import atproto/xrpc.{type XrpcError}
import gleam/dynamic/decode
import gleam/http/request
import gleam/http/response.{type Response}
import gleam/list
import gleam/option.{type Option, None}
pub type AuthServerMetadata {
AuthServerMetadata(
issuer: String,
authorization_endpoint: String,
token_endpoint: String,
pushed_authorization_request_endpoint: String,
revocation_endpoint: Option(String),
)
}
fn protected_resource_decoder() -> decode.Decoder(List(String)) {
decode.at(["authorization_servers"], decode.list(decode.string))
}
fn auth_server_decoder() -> decode.Decoder(AuthServerMetadata) {
use issuer <- decode.field("issuer", decode.string)
use authorization_endpoint <- decode.field(
"authorization_endpoint",
decode.string,
)
use token_endpoint <- decode.field("token_endpoint", decode.string)
use par <- decode.field(
"pushed_authorization_request_endpoint",
decode.string,
)
use revocation_endpoint <- decode.optional_field(
"revocation_endpoint",
None,
decode.optional(decode.string),
)
decode.success(AuthServerMetadata(
issuer:,
authorization_endpoint:,
token_endpoint:,
pushed_authorization_request_endpoint: par,
revocation_endpoint:,
))
}
pub fn fetch_protected_resource(
pds: String,
) -> Effect(Result(List(String), XrpcError)) {
use resp <- effect.then(get(pds <> "/.well-known/oauth-protected-resource"))
effect.done(parse(resp, protected_resource_decoder()))
}
pub fn fetch_authorization_server(
issuer: String,
) -> Effect(Result(AuthServerMetadata, XrpcError)) {
use resp <- effect.then(get(
issuer <> "/.well-known/oauth-authorization-server",
))
effect.done(parse(resp, auth_server_decoder()))
}
/// Resolve a PDS to its authorization server's endpoints in one call.
pub fn discover(pds: String) -> Effect(Result(AuthServerMetadata, XrpcError)) {
use servers <- effect.then(fetch_protected_resource(pds))
case servers {
Error(e) -> effect.done(Error(e))
Ok(servers) ->
case list.first(servers) {
Error(_) ->
effect.done(
Error(xrpc.DecodeFailed(
"no authorization_servers in protected-resource metadata",
)),
)
Ok(issuer) -> fetch_authorization_server(issuer)
}
}
}
fn get(url: String) -> Effect(Result(Response(String), XrpcError)) {
case request.to(url) {
Error(_) -> effect.done(Error(xrpc.RequestFailed(xrpc.InvalidUrl(url))))
Ok(req) -> {
use resp <- effect.then(effect.fetch(req))
case resp {
Error(e) -> effect.done(Error(xrpc.RequestFailed(e)))
Ok(resp) -> effect.done(xrpc.check_ok(resp))
}
}
}
}
fn parse(
resp: Result(Response(String), XrpcError),
decoder: decode.Decoder(a),
) -> Result(a, XrpcError) {
case resp {
Error(e) -> Error(e)
Ok(resp) -> xrpc.parse(resp.body, decoder)
}
}