Header menu logo FSharp.ATProto

Service Auth

AT Protocol backend services -- feed generators, labelers, and other service providers -- authenticate to each other using signed JWTs. The ServiceAuth module in FSharp.ATProto.Core handles creating, parsing, and validating these tokens, and can configure an AtpAgent to attach service auth headers automatically.

Algorithm

type Algorithm = ES256 | ES256K

ES256 corresponds to P-256 ECDSA, ES256K to secp256k1 ECDSA. Most AT Protocol services use ES256.

Claims

Service auth JWTs carry these claims:

type Claims =
    { Iss : Did               // Issuer -- your service's DID
      Aud : Did               // Audience -- the target service's DID
      Lxm : Nsid option       // Lexicon method being called (optional)
      Exp : DateTimeOffset    // Expiration time
      Iat : DateTimeOffset }  // Issued-at time

Creating Tokens

The createToken function takes a signing function (byte[] -> byte[]) that should produce a 64-byte compact ECDSA signature. If you are using FSharp.ATProto.Crypto, this is Signing.sign keyPair.

open FSharp.ATProto.Core
open FSharp.ATProto.Syntax
open FSharp.ATProto.Crypto

let keyPair = Keys.generate Algorithm.P256
let sign = Signing.sign keyPair
let iss = Did.parse "did:web:feed.example.com" |> Result.defaultWith failwith
let aud = Did.parse "did:plc:target-pds" |> Result.defaultWith failwith

// Full control over claims
let claims : ServiceAuth.Claims =
    { Iss = iss
      Aud = aud
      Exp = DateTimeOffset.UtcNow.AddMinutes 5.0
      Iat = DateTimeOffset.UtcNow
      Lxm = Nsid.parse "app.bsky.feed.getFeedSkeleton" |> Result.toOption }

let token = ServiceAuth.createToken ServiceAuth.Algorithm.ES256 sign claims

For the common case where you want a token that expires in 60 seconds:

let token2 =
    ServiceAuth.createTokenNow
        ServiceAuth.Algorithm.ES256
        sign
        iss
        aud
        (Nsid.parse "app.bsky.feed.getFeedSkeleton" |> Result.toOption)

Parsing and Validating Tokens

Parse claims from a JWT without verifying the signature:

match ServiceAuth.parseClaims token with
| Ok (claims, alg) ->
    printfn "Issuer: %s" (Did.value claims.Iss)
    printfn "Algorithm: %A" alg
| Error msg ->
    printfn "Parse error: %s" msg

Validate a JWT by verifying both the signature and the expiration:

let verifyFn = Signing.verify (Keys.publicKey keyPair)
match ServiceAuth.validateToken verifyFn token with
| Ok claims -> printfn "Valid token from %s" (Did.value claims.Iss)
| Error msg -> printfn "Invalid: %s" msg

validateToken returns Error if the signature is invalid or the token has expired.

Agent Integration

withServiceAuth configures an AtpAgent to automatically attach a Bearer token to every request. The NSID is extracted from the request URL path, so each request gets a correctly scoped token.

open FSharp.ATProto.Core
open FSharp.ATProto.Crypto

let keyPair = Keys.generate Algorithm.P256
let sign = Signing.sign keyPair
let agent =
    AtpAgent.create "https://bsky.social"
    |> ServiceAuth.withServiceAuth
        ServiceAuth.Algorithm.ES256
        sign
        iss   // your service's DID
        aud   // the target PDS DID

The agent will generate a fresh JWT for each request with a 60-second expiry and the correct lxm claim.

type Algorithm = | ES256 | ES256K
type Claims = { Iss: obj Aud: obj Lxm: obj Exp: obj Iat: obj }
type 'T option = Option<'T>
Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
namespace FSharp.ATProto
namespace FSharp.ATProto.Syntax
namespace FSharp.ATProto.Core
namespace System
type Algorithm = | P256
union case Algorithm.P256: Algorithm
type KeyPair = { Dummy: int }
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
val generate: _alg: Algorithm -> KeyPair
val _alg: Algorithm
module Unchecked from Microsoft.FSharp.Core.Operators
val defaultof<'T> : 'T
val sign: _kp: Keys.KeyPair -> (byte array -> byte array)
val _kp: Keys.KeyPair
module Keys from Service-auth.Crypto
Multiple items
val byte: value: 'T -> byte (requires member op_Explicit)

--------------------
type byte = Byte

--------------------
type byte<'Measure> = byte
val verify: _pub: unit -> _data: byte array -> _sig: byte array -> bool
val _pub: unit
type unit = Unit
val _data: byte array
val _sig: byte array
type bool = Boolean
val keyPair: Crypto.Keys.KeyPair
module Crypto from Service-auth
val generate: _alg: Crypto.Keys.Algorithm -> Crypto.Keys.KeyPair
union case Crypto.Keys.Algorithm.P256: Crypto.Keys.Algorithm
val sign: (byte array -> byte array)
module Signing from Service-auth.Crypto
val sign: _kp: Crypto.Keys.KeyPair -> (byte array -> byte array)
namespace Microsoft.FSharp
val keyPair: obj
val sign: obj
val iss: Did
Multiple items
module Did from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="Did" /> values. </summary>

--------------------
type Did = private | Did of string override ToString: unit -> string
<summary> A decentralized identifier (DID) as defined by the AT Protocol. DIDs are the primary stable identifier for accounts. Two methods are currently supported: <c>did:plc:</c> (hosted, managed by PLC directory) and <c>did:web:</c> (self-hosted, DNS-based). </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/did and the W3C DID specification: https://www.w3.org/TR/did-core/ </remarks>
val parse: s: string -> Result<Did,string>
<summary> Parse and validate a DID string. </summary>
<param name="s"> A DID string in the format <c>did:&lt;method&gt;:&lt;method-specific-id&gt;</c> (e.g. <c>"did:plc:z72i7hdynmk6r22z27h6tvur"</c> or <c>"did:web:example.com"</c>). </param>
<returns><c>Ok</c> with a validated <see cref="Did" />, or <c>Error</c> with a message describing the validation failure. Validation failures include: null input, exceeding the 2048-character limit, invalid DID syntax, or invalid percent-encoding. </returns>
<example><code> match Did.parse "did:plc:z72i7hdynmk6r22z27h6tvur" with | Ok did -&gt; printfn "Valid: %s" (Did.value did) | Error e -&gt; printfn "Invalid: %s" e </code></example>
Multiple items
module Result from Microsoft.FSharp.Core

--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
val defaultWith: defThunk: ('Error -> 'T) -> result: Result<'T,'Error> -> 'T
val failwith: message: string -> 'T
val aud: Did
val claims: ServiceAuth.Claims
module ServiceAuth from FSharp.ATProto.Core
<summary> Service-to-service JWT authentication for AT Protocol backend services (labelers, feed generators, etc.). </summary>
type Claims = { Iss: Did Aud: Did Lxm: Nsid option Exp: DateTimeOffset Iat: DateTimeOffset }
<summary> Claims in a service auth JWT. </summary>
Multiple items
[<Struct>] type DateTimeOffset = new: date: DateOnly * time: TimeOnly * offset: TimeSpan -> unit + 8 overloads member Add: timeSpan: TimeSpan -> DateTimeOffset member AddDays: days: float -> DateTimeOffset member AddHours: hours: float -> DateTimeOffset member AddMicroseconds: microseconds: float -> DateTimeOffset member AddMilliseconds: milliseconds: float -> DateTimeOffset member AddMinutes: minutes: float -> DateTimeOffset member AddMonths: months: int -> DateTimeOffset member AddSeconds: seconds: float -> DateTimeOffset member AddTicks: ticks: int64 -> DateTimeOffset ...
<summary>Represents a point in time, typically expressed as a date and time of day, relative to Coordinated Universal Time (UTC).</summary>

--------------------
DateTimeOffset ()
DateTimeOffset(dateTime: DateTime) : DateTimeOffset
DateTimeOffset(dateTime: DateTime, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(ticks: int64, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(date: DateOnly, time: TimeOnly, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, calendar: Globalization.Calendar, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, microsecond: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, microsecond: int, calendar: Globalization.Calendar, offset: TimeSpan) : DateTimeOffset
property DateTimeOffset.UtcNow: DateTimeOffset with get
<summary>Gets a <see cref="T:System.DateTimeOffset" /> object whose date and time are set to the current Coordinated Universal Time (UTC) date and time and whose offset is <see cref="F:System.TimeSpan.Zero" />.</summary>
<returns>An object whose date and time is the current Coordinated Universal Time (UTC) and whose offset is <see cref="F:System.TimeSpan.Zero" />.</returns>
DateTimeOffset.AddMinutes(minutes: float) : DateTimeOffset
Multiple items
module Nsid from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="Nsid" /> values. </summary>

--------------------
type Nsid = private | Nsid of string override ToString: unit -> string
<summary> A Namespaced Identifier (NSID) used to reference Lexicon schemas in the AT Protocol. NSIDs follow the pattern <c>authority.name</c> where the authority is a reversed domain name and the name identifies the specific schema (e.g. <c>app.bsky.feed.post</c>). Maximum length is 317 characters. </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/nsid </remarks>
val parse: s: string -> Result<Nsid,string>
<summary> Parse and validate an NSID string. </summary>
<param name="s"> An NSID string in the format <c>domain.segments.name</c> (e.g. <c>"app.bsky.feed.post"</c>). Must have at least three segments, where the final segment (name) starts with a letter and the preceding segments form a valid reversed domain authority. </param>
<returns><c>Ok</c> with a validated <see cref="Nsid" />, or <c>Error</c> with a message describing the validation failure. Validation failures include: null input, exceeding the 317-character limit, or invalid NSID syntax. </returns>
<example><code> match Nsid.parse "app.bsky.feed.post" with | Ok nsid -&gt; printfn "Authority: %s, Name: %s" (Nsid.authority nsid) (Nsid.name nsid) | Error e -&gt; printfn "Invalid: %s" e </code></example>
val toOption: result: Result<'T,'Error> -> 'T option
val token: string
val createToken: alg: ServiceAuth.Algorithm -> sign: (byte array -> byte array) -> claims: ServiceAuth.Claims -> string
<summary> Create a service auth JWT token. The sign function should produce a 64-byte compact ECDSA signature over the input bytes. </summary>
type Algorithm = | ES256 | ES256K
<summary> JWT signing algorithm. </summary>
union case ServiceAuth.Algorithm.ES256: ServiceAuth.Algorithm
val token2: string
val createTokenNow: alg: ServiceAuth.Algorithm -> sign: (byte array -> byte array) -> iss: Did -> aud: Did -> lxm: Nsid option -> string
<summary> Create a service auth JWT with default timing (issued now, expires in 60 seconds). </summary>
val parseClaims: token: string -> Result<(ServiceAuth.Claims * ServiceAuth.Algorithm),string>
<summary> Parse JWT claims without verifying the signature. </summary>
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
val alg: ServiceAuth.Algorithm
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
val value: Did -> string
<summary> Extract the string representation of a DID. </summary>
<param name="did">The DID to extract the value from.</param>
<returns>The full DID string (e.g. <c>"did:plc:z72i7hdynmk6r22z27h6tvur"</c>).</returns>
ServiceAuth.Claims.Iss: Did
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val msg: string
val verifyFn: (byte array -> byte array -> bool)
val verifyFn: obj
val validateToken: verify: (byte array -> byte array -> bool) -> token: string -> Result<ServiceAuth.Claims,string>
<summary> Validate a service auth JWT: verify signature, check expiry. The verify function should check a 64-byte compact ECDSA signature against data. </summary>
val agent: AtpAgent
Multiple items
module AtpAgent from FSharp.ATProto.Core
<summary> Functions for creating and authenticating <see cref="AtpAgent" /> instances. </summary>

--------------------
type AtpAgent = { HttpClient: HttpClient mutable BaseUrl: Uri mutable Session: AtpSession option ExtraHeaders: (string * string) list AuthenticateRequest: (HttpRequestMessage -> unit) option RefreshAuthentication: (unit -> Task<Result<unit,XrpcError>>) option OnSessionChanged: (unit -> unit) option }
<summary> Client agent for communicating with an AT Protocol Personal Data Server (PDS). Holds the HTTP client, base URL, optional authenticated session, and extra headers. </summary>
<remarks> Create an agent with <see cref="AtpAgent.create" /> or <see cref="AtpAgent.createWithClient" />, then authenticate with <see cref="AtpAgent.login" />. The agent's <see cref="Session" /> field is mutable: it is updated automatically on login and token refresh. </remarks>
<example><code> let agent = AtpAgent.create "https://bsky.social" let! session = AtpAgent.login "my-handle.bsky.social" "app-password-here" agent </code></example>
val create: baseUrl: string -> AtpAgent
<summary> Creates a new unauthenticated agent pointing at the given PDS base URL. A new <see cref="System.Net.Http.HttpClient" /> is allocated internally. </summary>
<param name="baseUrl"> The PDS base URL (e.g. <c>"https://bsky.social"</c>). A trailing slash is appended if not present. </param>
<returns>An unauthenticated <see cref="AtpAgent" /> ready for <see cref="login" />.</returns>
<example><code> let agent = AtpAgent.create "https://bsky.social" </code></example>
val withServiceAuth: alg: ServiceAuth.Algorithm -> sign: (byte array -> byte array) -> iss: Did -> aud: Did -> agent: AtpAgent -> AtpAgent
<summary> Configure an AtpAgent to use service auth for requests. The sign function should produce a 64-byte compact ECDSA signature. </summary>

Type something to start searching.