Connecting to Bluesky
Authenticating with handles and passwords
As you can see from the Hello World example connecting to Bluesky consists of creating an instance of a BlueskyAgent
and then calling the login method.
using (BlueskyAgent agent = new ())
{
var loginResult = await agent.Login(handle, password);
if (loginResult.Succeeded)
{
// Do your Bluesky thing
}
}
When a login is successful the agent will store the information needed for subsequent API calls in its Credentials
property, exchanging the handle
and password for tokens. API calls that require authentication will use this information and the access tokens will, by default, refresh automatically.
Important
If you are writing an application or web service you shouldn't save your users' passwords. The agent has events that allow you to react to logins, logouts and token refreshes to allow you to save authentication tokens rather than credentials.
If a user has email based two-factor authentication logins need an extra step.
The first login attempt will fail with an AuthFactorTokenRequired
error, at which point you should prompt the user to enter their sign-in code,
and call Login
again, this time with the username, password and the sign-in code.
var loginResult = await agent.Login(handle, password);
if (!loginResult.Succeeded &&
string.Equals(
loginResult.Error.Error!,
"AuthFactorTokenRequired",
StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Account requires an email authentication code.");
Console.WriteLine("Enter the email authentication code or press return to exit:");
string? emailAuthenticationCode = Console.ReadLine();
if (!string.IsNullOrEmpty(emailAuthenticationCode))
{
// Try again with the auth code.
loginResult = await agent.Login(handle, password, emailAuthenticationCode);
}
}
Tip
Bluesky allows you to create "app passwords", which you can use instead of your real password. Try using an app password in the sample above. If you have multi-factor authentication enabled on Bluesky app passwords don't require MFA.
Some users run their own Personal Data Servers, and/or have an decentralized identifier (DID) that isn't part
of the directory Bluesky runs. Whilst you can simply ask a user for their PDS location
you can also use the ResolveHandle
method in the AtProtoAgent
class to resolve a handle to a DID, and then use the
ResolveDIDDocument
method in the DirectoryServer
to discover the location of their PDS. Once you have a user's PDS all write, update and delete operations
should be performed against that PDS. The AtProtoAgent
Login
method does this behind the scenes so you don't have to.
Authenticating with OAuth
A more secure alternative to handles and passwords is OAuth. OAuth is a standard that allows users to grant access to their resources without having to share their credentials with an application. To use OAuth your application prepares a login URI and redirects the user to it, or in the case of desktop apps, opens a browser window to it. The user logs into Bluesky if needed then authorizes your application to access their resources.
Bluesky's OAuth implementation is, at the time of writing, very much under development. Currently Bluesky has three different resources, or "scopes",
atproto
: which gives your application the unique identifier for the user. This could be used for "Login with Bluesky" type functionality. This scope must always be requested.transition:generic
: which gives your application access to all permissions except for direct message access.transition:chat.bsky
: which gives your application direct message access.
Your application must have a client id and publish a metadata document in the format required by the ATProto OAuth specification.
For development a client it of http://localhost
is special cased by the specification, allowing you to develop your application without the need to have
published your application metadata file.
To use OAuth first configure the OAuth options for your agent. The options require the application ClientId
and the Scopes
your application requires,
and the the ReturnUri
from which your application will process OAuth logins. For web applications this will be a web page, for desktop applications
this is typically a custom uri scheme you have registered with the OS.
var agent = new BlueskyAgent(new BlueskyAgentOptions()
{
OAuth = new BlueskyOAuthOptions()
{
ClientId = "https://example.com",
Scopes = new[] { "atproto", "transition:generic" },
ReturnUri = new Uri("https://example.com/oauth/callback")
}
});
To start an OAuth login process you must first create an instance of OAuthClient
build a URI to send the user to, save the state from the
OAuthClient and then send the user to the URI. The user will log into Bluesky and authorize your application, and then be redirected back to your application
OAuthClient oAuthClient = agent.CreateOAuthClient();
Uri startUri = await agent.BuildOAuth2LoginUri(oAuthClient, handle, cancellationToken: cancellationToken);
// Save the state, and persist it in whatever way is suitable for your application,
// to be used when the response comes back from the OAuth server.
OAuthLoginState oAuthLoginState = uriBuilderOAuthClient.State;
// Send the user to the startUri in a way suitable for your application,
// a redirection for web application or spawning a browser for a desktop application.
When the user returns to your application you take the callback data returned from the OAuth server and process it
// Create an oauth client using the saved state
OAuthClient oAuthClient = agent.CreateOAuthClient(oAuthLoginState);
// Process the response
bool authenticated = await agent.ProcessOAuth2LoginResponse(oAuthClient, callbackData, cancellationToken);
The mechanisms for getting the login callback data, saving the state and restoring it vary due to application type.
Please consult the documentation for your application architecture. The Uri
returned by BuildOAuth2LoginUri
will
contain a state
query parameter, which can use as a primary key as needed for persisting the client state. You can extract this using
string stateKey = QueryHelpers.ParseQuery(startUri.Query)["state"]!;
.
Testing OAuth locally with localhost
The idunno.AtProto.OAuthCallback
nuget package contains a simple web server that can be used to test OAuth logins locally. To use it add a reference
to the package, set the ClientId in options to "http://localhost
" but do not set the ReturnUri, then create an instance of the callback server
before you build the login URI, use the callback server uri when creating the login URI, and finally await the callback,
which will return the callback data as a string
var agent = new BlueskyAgent(new BlueskyAgentOptions()
{
OAuth = new BlueskyOAuthOptions()
{
ClientId = "http://localhost",
Scopes = new[] { "atproto", "transition:generic" },
}
});
string callbackData;
await using var callbackServer = new CallbackServer(
CallbackServer.GetRandomUnusedPort(),
loggerFactory: loggerFactory);
{
OAuthClient uriBuilderOAuthClient = agent.CreateOAuthClient();
// We dynamically set the return URI as the callback server will listen on a random free port.
Uri startUri = await agent.BuildOAuth2LoginUri(
uriBuilderOAuthClient,
handle,
returnUri: callbackServer.Uri,
cancellationToken: cancellationToken);
// Start the browser. If you are running Linux you need XDG installed.
OAuthClient.OpenBrowser(startUri);
callbackData = await callbackServer.WaitForCallbackAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
}
if (!string.IsNullOrEmpty(callbackData))
{
await agent.ProcessOAuth2LoginResponse(oAuthClient, callbackData, cancellationToken);
}
else
{
// The process timed out, or another error occured.
}
The OAuth Sample shows how to use the callback server and login, and logout with OAuth.
Logging out
To log the current user off Bluesky call the Logout()
method. This revokes the refresh token (and, if they authenticated via OAuth, the access token),
and clears the now revoked credentials from the agent.
Configuring the agent's HTTP settings
The constructor for the Bluesky agents take an instance of BlueskyOptions
which allows for configuration of the agents. The BlueskyOptions
class
contains an HttpClientOptions
property which allows you to specify options for the underlying
HttpClients used to make requests and receive responses.
Configuring HTTP timeouts
Use the Timeout
options on HttpClientOptions
when creating an instance of the agent and provide a TimeSpan
to set the amount of time to wait before the request times out. For example, the following code will configure the agent to wait one minute for any server
it makes requests against to respond.
using (var agent = new BlueskyAgent(new BlueskyAgentOptions()
{
HttpClientOptions = new HttpClientOptions()
{
Timeout = TimeSpan.FromMinutes(1)
}
}))
{
}
Setting the user agent
Each request the agent makes is stamped with a string indicating the identity of the software making the request. By default this value is set to
idunno.AtProto/x.x.x
, where x.x.x is the version of the library being used. This is sent as the
UserAgent HTTP header in every request.
You should set the HttpClientOptions
HttpUserAgent
property to be a value indicating your own software's identity.
Using a proxy server
The HttpClientOptions
ProxyUri
property allows you to set an proxy to be used by the agent when making outgoing HTTP requests.
If you are using a debugging proxy such as Fiddler or Burp Suite it is
likely that you will also need to set the CheckCertificateRevocationList
property to false
,
Caution
Setting CheckCertificateRevocationList
property on HttpClientOptions
to false
is dangerous, as the client will no longer check if the
HTTPS certificate on any server it connects to has been revoked.
Only use set this to false
when you are using a debugging proxy which does not support CRLs.
// Disabling certification revocation list checks can introduce security vulnerabilities.
// Only use this setting when using a debugging proxy such as Fiddler or Burp Suite.
using (var agent = new BlueskyAgent(new BlueskyAgentOptions()
{
HttpClientOptions = new HttpClientOptions()
{
ProxyUri = new Uri("http://localhost:8866"),
CheckCertificateRevocationList = false
}
}))
{
}
Disabling token refresh
If you want to disable automatic authentication token refresh in an agent you can do that by the EnableTokenRefresh
property in options to false.
Eventually the access token will expire and APIs will start returning errors. You can call RefreshCredentials()
to refresh the access token manually.
var options = new BlueskyAgentOptions() { EnableBackgroundTokenRefresh = false };
using (BlueskyAgent agent = new (options)
{
// No token refresh will occur, so eventually API calls will fail.
}