Authentication Strategies

Authentication strategies for registering and linking accounts

Authentication strategies

An authentication strategy is a set of functions for registering and linking a user's account across devices.

export type AuthenticationStrategy = {
  implementation: Auth.Implementation<Components>

  accountConsumer: (username: string) => Promise<AccountLinkingConsumer>
  accountProducer: (username: string) => Promise<AccountLinkingProducer>
  isUsernameAvailable: (username: string) => Promise<boolean>
  isUsernameValid: (username: string) => Promise<boolean>
  register: (options: { username: string; email?: string }) => Promise<{ success: boolean }>
  session: () => Promise<Maybe<Session>>
}

An ODD program includes an authentication strategy. The ODD SDK provides a WebCrypto authentication strategy by default, but you can configure the strategy using the Components API.

An authentication strategy includes:

  • accountConsumer. An event emitter that requests device linking from an authed device.

  • accountProducer. An event emitter that provides device linking to an accountConsumer.

  • isUsernameAvailable. Checks if a username is available.

  • isUsernameValid. Checks if a username is valid.

  • register. Registers a user with a username and an optional email.

  • session. Creates a session. Called after registration or device linking.

UCAN. The following sections assume you have a working knowledge of User Controlled Authorization Networks (UCANs). If UCANs are unfamiliar, we strongly recommend you visit ucan.xyz to learn more before continuing.

Registering a user

The ODD SDK registers a user by submitting a username, optional email, and a UCAN authorizing the registration with the Fission server. The UCAN contains a decentralized identifier (DID) which the server registers as the user's account DID.

Here is how you register a user:

const username = "llama"

// Check if username is valid and available
const valid = program.auth.isUsernameValid(username)
const available = await program.auth.isUsernameAvailable(username)

if (valid && available) {
  // Register the user
  const { success } = await program.auth.register({ username })
  
  // Create a session on success
  const session = success ? program.auth.session() : null
}

Our example checks if a username is valid and available, registers a user, and creates a session.

Why is email optional? Most ODD applications will not need to register user emails. However, we plan to add support for account recovery in a future version of the ODD SDK, and email may become more relevant then.

Account and Agent DIDs

Each of a user's devices has an agent DID associated with the key pair on device. You can query the program to inspect the agent DID:

const agentDID = await program.agentDID()

The account DID is the agent DID on the device where the user registered. Each time a device makes a request to the Fission server on behalf of the user, it sends a UCAN issued by the account DID or by an agent DID that has authority delegated by the account DID. We'll see how authority can be delegated in the Device Linking section.

You can query the program for the account DID on any device:

const accountDID = await program.accountDID(username)

Device linking

Device linking provides a secure mechanism for linking a user's account across devices or browsers.

Users link devices mainly to access their data on their phone, laptop, tablet, or in another browser. A second benefit is that linked devices are an account backup mechanism. Any linked device can always authorize a new device if a user loses their root device. See the Fission Account Linking guide for an example walkthrough of a device linking experience.

Device linking occurs between an account producer (an authed device) and an account consumer (a device that would like to be linked). The devices bootstrap a secure channel to communicate, and the producer issues an account UCAN to the consumer that authorizes it to act on the user's behalf with full capability for 1000 years. So effectively, the newly linked device will have full access to the user's account for as long as it matters.

No private keys are ever sent over the wire during device linking. Instead, each device is an agent with its own key pair and associated agent DID. The producer issues an account UCAN that delegates authority to the consumer's agent DID. When the consumer later makes a request to the Fission server, it provides the account UCAN as proof that it can act on the user's behalf.

Distributed applications. We've claimed a few times that the ODD SDK is an SDK for building distributed applications. Device linking uses the AWAKE protocol and is one piece at the heart of what makes ODD apps distributed. An authed device has all the authority it needs to link new devices. They do not need to ask a server or third party for permission.

When a user links a device, they should open a linking page on the authed device and the device they would like to link. The device to be linked needs to be configured with their username. We recommend the authed device display a QRCode or copy link that encodes the username as a query string param. The user can also manually enter their username on the device to be linked.

During device linking, interfaces must present the user with a PIN challenge. Display the PIN challenge on both devices so the user can confirm the PINs match. The PIN challenge is important for securing the device linking process to prevent person-in-the-middle attacks.

Here is an example of an account producer:

// The producer should already have an active session
const producer = await program.auth.accountProducer(program.session.username)

// The producer receives a challenge PIN from the consumer
producer.on("challenge", challenge => {
  // Either show `challenge.pin` or have the user input a PIN and check if they're equal.
  if (userInput === challenge.pin) challenge.confirmPin()
  else challenge.rejectPin()
})

// The producer reports whether a user approved or rejected
producer.on("link", ({ approved }) => {
  if (approved) console.log("Linked device successfully")
})

An account producer emits challenge and link events. The challenge is a PIN sent by the consumer.

Here is a matching account consumer:

// The username can be transmitted in a QRCode or a copy link
// Alternatively, the user can enter the username into an input
const consumer = await program.auth.accountConsumer(username)

// The consumer generates a PIN and sends it to the producer
consumer.on("challenge", ({ pin }) => {
  // Display the PIN
  showPinOnUI(pin)
})

// The consumer receives an approval or rejection message from the producer
consumer.on("link", async ({ approved, username }) => {
  if (approved) {
    console.log(`Successfully authenticated as ${username}`)
    session = await program.auth.session()
  }
})

An account consumer also emits challenge and link events. The consumer should create a session when the link event reports approval.

Last updated