Current section
Files
Jump to
Current section
Files
src/atproto/oauth/core/flow.gleam
//// Start an authorization flow as an `Effect`: resolve the PDS, discover the
//// authorization server, push the request (PAR) with PKCE + DPoP, and return
//// the authorization URL plus the discovered endpoints. The one-shot inputs
//// the platform computes (PKCE challenge, `state`) come in as parameters; the
//// DPoP key never appears here (PAR's proof is a `DpopProof` effect). The
//// per-target wrapper adds the key and its own `Flow` record.
import atproto/oauth/core/effect.{type Effect}
import atproto/oauth/core/metadata.{type AuthServerMetadata}
import atproto/oauth/core/transport
import atproto/xrpc
import gleam/dynamic/decode
import gleam/http/request
import gleam/int
import gleam/list
import gleam/string
import gleam/uri
pub type StartError {
ResolveFailed(String)
DiscoverFailed(String)
ParFailed(String)
}
/// What the kernel resolves before the wrapper folds in its platform key: the
/// authorization redirect URL and the discovered endpoints.
pub type Authorized {
Authorized(
redirect_url: String,
pds: String,
issuer: String,
token_endpoint: String,
)
}
pub fn start(
resolver resolver: String,
identifier identifier: String,
client_id client_id: String,
redirect_uri redirect_uri: String,
scope scope: String,
pkce_challenge pkce_challenge: String,
state state: String,
extra_form extra_form: List(#(String, String)),
) -> Effect(Result(Authorized, StartError)) {
use pds <- effect.then(resolve_pds(resolver, identifier))
case pds {
Error(e) -> effect.done(Error(ResolveFailed(string.inspect(e))))
Ok(pds) -> {
use server_metadata <- effect.then(metadata.discover(pds))
case server_metadata {
Error(e) -> effect.done(Error(DiscoverFailed(string.inspect(e))))
Ok(server_metadata) -> {
let form =
list.append(
[
#("client_id", client_id),
#("response_type", "code"),
#("code_challenge", pkce_challenge),
#("code_challenge_method", "S256"),
#("redirect_uri", redirect_uri),
#("scope", scope),
#("state", state),
#("login_hint", identifier),
],
extra_form,
)
use request_uri <- effect.then(push_par(server_metadata, form))
case request_uri {
Error(e) -> effect.done(Error(e))
Ok(request_uri) -> {
let redirect_url =
server_metadata.authorization_endpoint
<> "?"
<> uri.query_to_string([
#("client_id", client_id),
#("request_uri", request_uri),
])
effect.done(
Ok(Authorized(
redirect_url:,
pds:,
issuer: server_metadata.issuer,
token_endpoint: server_metadata.token_endpoint,
)),
)
}
}
}
}
}
}
}
fn push_par(
server_metadata: AuthServerMetadata,
form: List(#(String, String)),
) -> Effect(Result(String, StartError)) {
use resp <- effect.then(transport.post_form_with_dpop(
server_metadata.pushed_authorization_request_endpoint,
form,
))
case resp {
Error(e) -> effect.done(Error(ParFailed(e)))
Ok(resp) ->
case resp.status >= 200 && resp.status < 300 {
True ->
effect.done(
case
xrpc.parse(resp.body, decode.at(["request_uri"], decode.string))
{
Ok(request_uri) -> Ok(request_uri)
Error(e) -> Error(ParFailed(string.inspect(e)))
},
)
False ->
effect.done(
Error(ParFailed(
"PAR " <> int.to_string(resp.status) <> ": " <> resp.body,
)),
)
}
}
}
fn resolve_pds(
resolver: String,
identifier: String,
) -> Effect(Result(String, xrpc.XrpcError)) {
let query = uri.query_to_string([#("identifier", identifier)])
let url =
resolver <> "/xrpc/com.bad-example.identity.resolveMiniDoc?" <> query
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) ->
case xrpc.check_ok(resp) {
Error(e) -> effect.done(Error(e))
Ok(resp) ->
effect.done(xrpc.parse(resp.body, mini_doc_pds_decoder()))
}
}
}
}
}
fn mini_doc_pds_decoder() -> decode.Decoder(String) {
use _did <- decode.field("did", decode.string)
use pds <- decode.field("pds", decode.string)
decode.success(pds)
}