#bluesky #oauth #config

atrium-oauth

Core library for implementing AT Protocol OAuth clients

2 releases

0.1.1 Apr 2, 2025
0.1.0 Apr 2, 2025

#164 in Authentication

Download history 291/week @ 2025-04-02

291 downloads per month

MIT license

1MB
24K SLoC

ATrium OAuth: atproto flavoured OAuth client

Core library for implementing atproto OAuth clients.

Usage

Configuration

use atrium_identity::{
    did::{CommonDidResolver, CommonDidResolverConfig, DEFAULT_PLC_DIRECTORY_URL},
    handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig, DnsTxtResolver},
};
use atrium_oauth::{
    store::{session::MemorySessionStore, state::MemoryStateStore},
    AtprotoLocalhostClientMetadata, DefaultHttpClient, KnownScope, OAuthClient, OAuthClientConfig,
    OAuthResolverConfig, Scope,
};
use std::{error::Error, sync::Arc};

struct SomeDnsTxtResolver;

impl DnsTxtResolver for SomeDnsTxtResolver {
    async fn resolve(
        &self,
        _: &str,
    ) -> Result<Vec<String>, Box<dyn Error + Send + Sync + 'static>> {
        todo!()
    }
}

fn main() {
    let http_client = Arc::new(DefaultHttpClient::default());
    let config = OAuthClientConfig {
        client_metadata: AtprotoLocalhostClientMetadata {
            redirect_uris: Some(vec![String::from("http://127.0.0.1/callback")]),
            scopes: Some(vec![
                Scope::Known(KnownScope::Atproto),
                Scope::Known(KnownScope::TransitionGeneric),
            ]),
        },
        keys: None,
        resolver: OAuthResolverConfig {
            did_resolver: CommonDidResolver::new(CommonDidResolverConfig {
                plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(),
                http_client: Arc::clone(&http_client),
            }),
            handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig {
                dns_txt_resolver: SomeDnsTxtResolver,
                http_client: Arc::clone(&http_client),
            }),
            authorization_server_metadata: Default::default(),
            protected_resource_metadata: Default::default(),
        },
        // A store for saving state data while the user is being redirected to the authorization server.
        state_store: MemoryStateStore::default(),
        // A store for saving session data.
        session_store: MemorySessionStore::default(),
    };
    let Ok(client) = OAuthClient::new(config) else {
        panic!("failed to create oauth client");
    };
}

Authentication

use atrium_oauth::{AuthorizeOptions, KnownScope, OAuthClient, Scope};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = OAuthClient::new(...)?;
    let url = client
        .authorize(
            "foo.bsky.team",
            AuthorizeOptions {
                scopes: vec![
                    Scope::Known(KnownScope::Atproto),
                    Scope::Known(KnownScope::TransitionGeneric),
                ],
                ..Default::default()
            },
        )
        .await?;

    ...

    Ok(())
}

Make user visit url. Then, once it was redirected to the callback URI, perform the following:

use atrium_api::agent::Agent;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = OAuthClient::new(...)?;

    ...

    let query_params = "code=...&state=...";
    let params = serde_html_form::from_str(query_params)?;
    let (oauth_session, _) = client.callback(params).await?;

    ...

    Ok(())
}

The sign-in process results in an OAuthSession instance that can be used to make authenticated requests to the resource server. This instance will automatically refresh the credentials when needed.

Making authenticated requests

The atrium_oauth package provides a OAuthSession class that can be used to make authenticated requests to Bluesky's AppView. This can be achieved by constructing an Agent instance using the OAuthSession instance.

use atrium_api::agent::Agent;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {

    ...

    let (oauth_session, _) = client.callback(params).await?;
    let agent = Agent::new(oauth_session);
    let output = agent
        .api
        .app
        .bsky
        .feed
        .get_timeline(
            atrium_api::app::bsky::feed::get_timeline::ParametersData {
                algorithm: None,
                cursor: None,
                limit: 3.try_into().ok(),
            }
            .into(),
        )
        .await?;
    for feed in &output.feed {
        println!("{feed:?}");
    }

    ...

    Ok(())
}

Dependencies

~13–46MB
~727K SLoC