Skip to content

stg-tud/daimpl-lofi-auth

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Local-First Data Management Project

0. About

This repo is part of the Local-First Data Management project for the Software Technology Group at TU Darmstadt.

We considered the local-first approach and decided to implement UCAN as our local-first solution with a sample notes application in Scala.
We selected UCAN v0.10.0 for the following reasons:

  1. The specification is more complete compared to newer versions.
  2. Existing libraries in other programming languages are also aligned with this version, ensuring compatibility.
  3. It is simpler to implement at this stage.

Team Members:

Supervisor:

Project-Website: Local-First Data Management

1. Build & Run

Important

The following commands were tested in the Windows Command Prompt. If you're using a different operating system, you may need to adjust them accordingly.

  1. Install Scala
  2. Install Node.js
  3. Clone the Repository(with HTTPS or SSH):
  • HTTPS:

    git clone https://github.com/stg-tud/daimpl-lofi-auth.git
  • SSH:

    git clone [email protected]:stg-tud/daimpl-lofi-auth.git
  1. Navigate into the project folder:
    cd daimpl-lofi-auth
  2. Run the Scala UCAN-Backend Server:
  • Once everything is set up, execute sbt run.

  • When executing this command, you may see the following prompt:

    Multiple main classes detected. Select one to run:
     [1] Main
     [2] ucan.UcanDemo
    
    Enter number:
  • Option [1] Main:
    Starts the server for the sample applications.

  • Option [2] ucan.UcanDemo:
    Runs a separate demo showcasing UCAN itself.

If you want to run tests, you need to execute sbt test to run all tests.

Run Pure UCAN Example App (A nice UI for testing all functionality)

First, make sure the Scala UCAN-Backend Server is running on port 8080.

  1. Open a new command prompt window and navigate into the project folder of the Node.js server:
    cd daimpl-lofi-auth\example-apps\
  2. Install Node.js dependencies:
    npm install
  3. Navigate to /UcanShowcase Run the Node.js server:
    cd /UcanShowcase
    node server.js
  4. Navigate to http://localhost:3000 in your web browser.

You can now play around with UCAN invocation/delegation/revocation. The visualization displays the UCANs in the current session and all relationships.

Run Example Notes App using UCAN

First, make sure the Scala UCAN-Backend Server is running on port 8080.

  1. Open a new command prompt window and navigate into the project folder of the Node.js server:
    cd daimpl-lofi-auth\example-apps\
  2. Install Node.js dependencies:
    npm install
  3. Navigate to /NotesApp Run the Node.js server:
    cd /NotesApp
    node server.js
  4. Navigate to http://localhost:5000 in your web browser.
  5. Login with the following credentials:
    • Username: admin
    • Password: admin

You can now perform CRUD operations on notes. If you want to register a new user or change the capabilities of a user, you can open the /admin path or use the header for navigation.

2. Code Example

In the below notes app example, Alice grants Bob full permissions (read, create, update) for notes. Bob then delegates a subset of these permissions to Carol (read, create) and Dan (read, update). Carol further delegates her permissions (read, create) to Dan. Finally, Dan combines all received permissions and grants Erin again comprehensive access (read, create, update).

Please note that if any link in a chain becomes invalid, alternative paths may still provide access (e.g. if Carol → Dan was invalid, Dan would still retain read and update capabilities from Bob directly).

               Root
        ┌────────────────┐
        │                │
        │  iss: Alice    │
        │  aud: Bob      │
        │  Resource:     |
        |  notes         |
        |  cap: [read,   |
        |       create,  |
        |       update]  | 
        └───┬────────┬───┘
            │        │
            │        │
            ▼        ▼
┌──────────────┐  ┌──────────────┐
│              │  │              │
│  iss: Bob    │  │  iss: Bob    │
│  aud: Carol  │  │  aud: Dan    │
│  Resource:   |  | Resource:    | 
|  notes       |  | notes        |
│  cap: [read, │  │  cap: [read, │
│       create]│  │       update]│
└───────┬──────┘  └──┬───────────┘
        │            │
        │            │
        ▼            │
┌──────────────┐     │
│              │     │
│  iss: Carol  │     │
│  aud: Dan    │     │
│  Resource:   |     |
|  notes       │     │
│  cap: [read, │     │
│       create]│     │
└───────────┬──┘     │
            │        │
            │        │
            ▼        ▼
        ┌────────────────┐
        │                │
        │  iss: Dan      │
        │  aud: Erin     │
        │  Resource:     |
        |  notes         │
        │  cap: [read,   │
        │        create, │
        │        update] │
        └────────────────┘

This UCAN library can model such structures like this:

// Create cryptographic key material for all participants
val alice = Ucan.createDefaultKeymaterial()
val bob = Ucan.createDefaultKeymaterial()
val carol = Ucan.createDefaultKeymaterial()
val dan = Ucan.createDefaultKeymaterial()
val erin = Ucan.createDefaultKeymaterial()

// Root UCAN: Alice grants Bob full permissions (read, create, update)
// Alice is the root authority in this system
val aliceBobPayload = Ucan
    .builder()
    .issuedBy(alice)
    .forAudience(bob)
    .withLifetime(3600) // Token valid for 1 hour
    .claimingCapability("notes", "read")
    .claimingCapability("notes", "create")
    .claimingCapability("notes", "update")
    .build()
val aliceBobUcan = Ucan.sign(aliceBobPayload, alice)
val aliceBobCid = Ucan.createCID(aliceBobUcan)
// created with a unique content identifier (CID)

// Bob delegates read and create permissions to Carol
// Note that Bob can only delegate permissions he has received from Alice
val bobCarolPayload = Ucan
    .builder()
    .issuedBy(bob)
    .forAudience(carol)
    .withLifetime(3000) // Token valid for 50 minutes
    .claimingCapability("notes", "read")
    .claimingCapability("notes", "create")
    .witnessedBy(aliceBobCid) // Proof that Bob has these permissions
    .build()
val bobCarolUcan = Ucan.sign(bobCarolPayload, bob)
val bobCarolCid = Ucan.createCID(bobCarolUcan)

// Bob delegates read and update permissions to Dan
// This creates a different permission set than what was given to Carol
val bobDanPayload = Ucan
    .builder()
    .issuedBy(bob)
    .forAudience(dan)
    .withLifetime(3000)
    .claimingCapability("notes", "read")
    .claimingCapability("notes", "update")
    .witnessedBy(aliceBobCid) 
    .build()
val bobDanUcan = Ucan.sign(bobDanPayload, bob)
val bobDanCid = Ucan.createCID(bobDanUcan)

// Carol delegates her permissions to Dan
// This creates a second path through which Dan receives read and create permissions
val carolDanPayload = Ucan
    .builder()
    .issuedBy(carol)
    .forAudience(dan)
    .withLifetime(2400)
    .claimingCapability("notes", "read")
    .claimingCapability("notes", "create")
    .witnessedBy(bobCarolCid) 
    .build()
val carolDanUcan = Ucan.sign(carolDanPayload, carol)
val carolDanCid = Ucan.createCID(carolDanUcan)

// Dan combines all permissions and delegates to Erin
// Dan now has read+create (from Carol) and read+update (from Bob)
// Together this gives Dan read+create+update, which he delegates to Erin
val danErinPayload = Ucan
    .builder()
    .issuedBy(dan)
    .forAudience(erin)
    .withLifetime(1800)
    .claimingCapability("notes", "read")
    .claimingCapability("notes", "create")
    .claimingCapability("notes", "update")
    .witnessedBy(carolDanCid)
    .witnessedBy(bobDanCid)
    .build()
val danErinUcan = Ucan.sign(danErinPayload, dan)
val danErinJwt = Ucan.encodeJwt(danErinUcan)

// Set up proof resolution system to validate the chain
// This maps CIDs to their corresponding JWT tokens for verification
val notesProofs = Map(
  aliceBobCid.encode() -> Ucan.encodeJwt(aliceBobUcan),
  bobCarolCid.encode() -> Ucan.encodeJwt(bobCarolUcan),
  bobDanCid.encode() -> Ucan.encodeJwt(bobDanUcan),
  carolDanCid.encode() -> Ucan.encodeJwt(carolDanUcan)
)
// Use In-Memory stores for resolving the CIDs. 
val notesRevocationStore = new Ucan.InMemoryRevocationStore()
val notesResolver = new Ucan.InMemoryProofResolver(notesProofs, notesRevocationStore)

// Initial validation (all delegation paths intact)
Ucan.validateToken(danErinJwt, notesResolver) match {
    case Success(capabilities) =>
        // Will show that Erin has read, create, and update abilities
        // Full notes access will be confirmed
    case Failure(e) =>
        // This branch won't be executed in the initial validation
}

// Revoking Carol -> Dan UCAN (removing one path for create capability)
// This simulates Carol revoking Dan's permissions
val carolDanRevocation = Ucan.createRevocation(carolDanCid, carol).get
notesRevocationStore.addRevocation(carolDanRevocation)

// Validation after revoking Carol -> Dan UCAN
// Dan still has read+update from Bob, but the create capability path is broken
Ucan.validateToken(danErinJwt, notesResolver) match {
    case Success(capabilities) =>
        // Will show that Erin now only has read and update abilities
        // Create ability will be missing because the Carol->Dan path was revoked
    case Failure(e) =>
        // This branch won't be executed yet, as there's still a valid path
}

// Revoking Bob -> Dan UCAN (removing the remaining valid path)
// This completely cuts off Dan's authority
val bobDanRevocation = Ucan.createRevocation(bobDanCid, bob).get
notesRevocationStore.addRevocation(bobDanRevocation)

// Validation after revoking both delegation paths to Dan
// At this point, there is no valid path from the root to Erin
Ucan.validateToken(danErinJwt, notesResolver) match {
    case Success(capabilities) =>
        // This branch won't be executed as validation will fail
    case Failure(e) =>
        // Will report that validation failed because all paths are invalid
}

This showcases a possible application for using UCANs. To see more of the functionality this UCAN library offers, please have a look at Demo.scala.

3. Status of This Project

Implementation Details

  • The application uses a simple in-memory database to store UCANs.
  • For an ability, no caveats are applied by default, meaning that the ability is always allowed.
  • Key generation is implemented using only the Ed25519 algorithm as a Public-Key System.
  • Aside from these points, the implementation aligns with the UCAN specification [2].

Limitations

  • The implementation does not support complex token resolution methods such as RESTful services.
  • Currently, only Ed25519 is used for key generation.
  • Backwards compatibility with older UCAN versions is not guaranteed, as comprehensive testing across versions was not feasible.
  • Some details like Section 4 ("Reserved Resources") of the specification are not implemented yet due to not being essential for basic UCAN usage. One may want to expand the library to implement this.
  • Furthermore, the use of Session Content Identifiers (CIDs) for message authentication is not implemented (each invocation requires the full UCAN).
  • Finally, certain limitations are inherent to UCAN itself, such as the lack of protection against man-in-the-middle (MITM) attacks and potential issues with outdated validations due to non-synchronized UCAN stores. These are further described in the UCAN Spec [2].

Future Work

  • One option is to use the current code as a base for implementing the latest version of UCAN, ensuring a straightforward upgrade path.
  • Another option is to adapt it into a library for local-first projects, enabling a modular and flexible approach to new functionalities.
  • The limitations mentioned above can also be addressed in future work.

4. Background

Local-First Data Management

Note

The following section about Local-First Data Management is based on the Ink & Switch article [1]. It contains the relevant background information for this project; for detailed information, please refer to the original article. Our goal is to provide a focused summary so that readers do not need to go through the entire specification, but can quickly understand the essential concepts relevant to this implementation.

What are the concrete advantages of local-first software?

  • No latency; There is never a need for the user to wait for a request to a server to complete
  • Local-first apps keep their data in local storage on every device
  • The user can read and write to local-first applications anytime, even while offline
  • Support for real-time collaboration that is on par with the best cloud apps today
  • Local-first software lets you keep using your data and apps forever, even if the company shuts down or stops supporting them.
  • Local-first apps are more private and secure because your data stays on your own devices and is not stored in a central database that hackers or companies can access

Any negatives?

  • Greater responsibility. The user must employ preventive measures to avoid data loss or randomware infections
  • Peer-to-peer systems are never fully “online” or “offline” and it can be hard to reason about how data moves in them

Evaluating existing tools

1. Fast 2. Multi-device 3. Offline 4. Collaboration 5. Longevity 6. Privacy 7. User control
Files + email attachments
Google Docs
Trello
Pinterest
Dropbox
Git+GitHub
Web apps
Thick client
Firebase, CloudKit, Realm
CouchDB
True local-first approach

Git+GitHub is the closest thing to a true local-first application in this table, though it has limitations for real-time collaboration and non-textual file formats and relies on repository hosting services like GitHub for collaboration and backup.

CRDTs

Conflict-free replicated data types (CRDTs) are an alternative to operational transformation (OT) for collaborative editing. The main difference between the two approaches is that OT focuses on transforming index positions to make sure all clients will have on the same content, while CRDTs use mathematical models (e.g. linked lists) that do not require index transformations.

As of right now, OT is the standard for shared text editing. The issue is that OT-based systems that support shared editing without a central server can become impractical because of the complex bookkeeping they require. CRDTs are a better approach for distributed systems, providing stronger guarantees for document synchronization across clients without the need for a central authority.

Example project by Ink & Switch

The Trellis project is a Kanban board modeled after Trello, using CRDTs to build a fast, multi-device, offline, collaborative, long-living, privacy-first, user-controlled application.

Here, they state: "Access permissions for documents beyond secret URLs remain an open research question". This is something UCAN may solve.

UCAN

Note

The following section about UCAN is based on the UCAN v0.10.0 Specification [2]. It contains the relevant background information for this project; for detailed information, please refer to the original article. Our goal is to provide a focused summary so that readers do not need to go through the entire specification, but can quickly understand the essential concepts relevant to this implementation.

User Controlled Authorization Network (UCAN) is a trustless, secure, local-first, user-originated, distributed authorization scheme.

When a central authority is present, using access control lists (ACLs) is common and good practice. However, when there is no such central authority (such as in local-first applications), authorization needs to be decentralized, offline-available and user-controlled (self-verifiable).

Motivation

Modern applications must flexibly define trustless authorization and handle hostile environments, prompting the adoption of strong public key solutions. UCAN leverages concepts from SPKI (Simple Public Key Infrastructure) and object capabilities (OCAP), following the “capabilities as certificates” approach—with offline functionality, self-verifiability, revocation, and stateful capabilities.

Roles

There are several roles that an agent MAY assume:

Name Description
Validator Any agent that interprets a UCAN to determine that it is valid, and which capabilities it grants
Audience The principal delegated to in the current UCAN. Listed in the aud field
Issuer The signer of the current UCAN. Listed in the iss field
Revoker The issuer listed in a proof chain that revokes a UCAN
Owner The root issuer of a capability, who has some proof that they fully control the resource

Resource

A resource is data or a process that has an address (e.g., database row, user account, or storage quota).

Lifecycle

Abilities define the actions that can be performed on a resource (e.g., GET, PUT, POST). Although you can define any semantics, they must logically apply to the resource. Abilities can be hierarchical.

A capability pairs an ability with a resource and optional caveats.

The set of capabilities delegated by a UCAN is referred to as its authority.

Delegation, Invocation, and Revocation

Delegation is the act of granting another principal (the delegate) the capability to use a resource on behalf of the delegator. This is authorized by a constructive "proof," which may be a signature from the owner or a UCAN that includes the relevant capability. Each delegation maintains or reduces the level of access, except in cases of rights amplification, where multiple proofs may be combined if allowed by the resource’s semantics.

Invocation is the actual use of a delegated capability to perform an operation on a resource.

Revocation is the invalidation of a UCAN after issuance, beyond built-in constraints like expiry. It must be performed by the issuer DID of the original proof, ensuring that only the original authority can revoke a delegated capability.

JWT Structure

UCANs are formatted as standard JWTs (JSON Web Tokens) and include a header, payload, and signature.

Header

The JWT header includes:

Field Type Description Required
alg String Signature algorithm Yes
typ String Type ("JWT") Yes

Example:

{
  "alg": "EdDSA",
  "typ": "JWT"
}
Payload

The payload carries the UCAN-specific claims:

Field Type Description Required
ucv String UCAN version (e.g., "0.2.0") Yes
iss String Issuer DID Yes
aud String Audience DID Yes
nbf Integer Not valid before (Unix timestamp) No
exp Integer or null Expiration time (Unix timestamp) Yes
nnc String Nonce for uniqueness No
fct Object Signed facts No
cap Object Capabilities (URI → abilities → caveats) Yes
prf Array of CIDs Proofs (delegated UCANs) No

Example:

"aud": "did:key:...",
"iss": "did:key:...",
"exp": 1700000000,
"cap": {
  "https://example.com": {
    "crud/read": [{}],
    "crud/update": [
      {"status": "draft"},
      {"status": "published", "day-of-week": "Monday"}
    ]
  }
}
Proofs

Proofs are links to previous UCANs that authorize the current one’s capabilities. Each entry in the prf array must be a CID pointing to another UCAN.

Example:

"prf": [
  "bafkrei...",
  "bafkrei..."
]

Validation

If any of the following criteria are not met, the UCAN MUST be considered invalid:

  1. Time
    A UCAN’s nbf (not before) and exp (expiry) define its valid window; usage outside this window invalidates the UCAN.

  2. Principal Alignment
    In delegation, each proof’s aud must match the next UCAN’s iss, forming a chain back to the resource owner.

  3. Proof Chaining
    Every delegated capability must either be issued by the resource owner or justified by proofs in the prf field, creating a chain to the root UCAN. Each step may only maintain or reduce capabilities (attenuation). Time bounds must also be within those of the proofs.

  4. Rights Amplification
    Some capabilities require multiple distinct proofs to be valid. This is only allowed if it’s specified in the resource’s semantics.

  5. Content Identifiers
    UCAN tokens are referenced by a base32 CIDv1 using SHA2-256 and the raw data multicodec (0x55). Other codecs may be used if the token remains a valid JWT. Resolution is left to the implementation (e.g., local store, DHT, or REST).

  6. Revocation
    The issuer of a UCAN can revoke it, or any further derived capabilities, in an eventually consistent manner. Revocations are ideally stored near the resource they affect and are irreversible; mistaken revocations require creating a new UCAN. Only proofs issued by the same DID are invalidated, leaving other valid chains intact. Time-based constraints remain a more proactive approach.

References

  1. Local-First Overview - Ink & Switch
  2. UCAN v0.10.0 Spec
  3. Local-First Enterprise Applications
    Wolski, André (2024)
    Local-First Enterprise Applications.
    Technische Universität Darmstadt
    doi: 10.26083/tuprints-00027006
    Master Thesis, Primary publication, Publisher's Version

About

[@haaase] DAIMPL project on local-first auth and data management (WiSe 24/25)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •