diff --git a/README.md b/README.md index fd0c560..db05031 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,226 @@ +![ticket](doc_assets/ticket-4.png?raw=true "ticket") + # ExUcan -**Decentralized Auth with [UCANs](https://ucan.xyz/)** +> Decentralized Auth with [UCANs](https://ucan.xyz/) -## Installation +**Elixir library to help the next generation of applications make use of UCANs in their authorization flows. To learn more about UCANs and how you might use them in your application, visit [https://ucan.xyz](https://ucan.xyz) or read the [spec](https://github.com/ucan-wg/spec).** + +## Table of Contents + +1. [About](#about) +2. [Structure](#structure) +3. [Installation](#installation) +4. [Usage](#usage) + - [Generating UCAN](#generating-ucan) + - [Validating UCAN](#validating-ucan) + - [Adding Capabilities](#adding-capabilities) +5. [Roadmap](#roadmap) + +## About +UCANs are JWTs that contain special keys pecifically designed to enable ways of authorizing offline-first apps and distributed systems. + +At a high level, UCANs (“User Controlled Authorization Network”) are an authorization scheme ("what you can do") where users are fully in control. UCANs use [DID](https://www.w3.org/TR/did-core/#:~:text=Decentralized%20identifiers%20(DIDs)%20are%20a,the%20controller%20of%20the%20DID.)s ("Decentralized Identifiers") to identify users and services ("who you are"). + +No all-powerful authorization server or server of any kind is required for UCANs. Instead, everything a user can do is captured directly in a key or token, which can be sent to anyone who knows how to interpret the UCAN format. Because UCANs are self-contained, they are easy to consume permissionlessly, and they work well offline and in distributed systems. + +UCANs work, + +Server → Server + +Client → Server + +Peer-to-peer + +**OAuth is designed for a centralized world, UCAN is the distributed user-controlled version.** + +## Structure + +### Header + + `alg`, Algorithm, the type of signature. + + `typ`, Type, the type of this data structure, JWT. + +### Payload -If [available in Hex](https://hex.pm/docs/publish), the package can be installed -by adding `ex_ucan` to your list of dependencies in `mix.exs`: + `ucv`, UCAN version. + + `cap`, A list of resources and capabilities that the ucan grants. + + `aud`, Audience, the DID of who it's intended for. + + `exp`, Expiry, unix timestamp of when the jwt is no longer valid. + + `fct`, Facts, an array of extra facts or information to attach to the jwt. + + `nnc`, Nonce value to increase the uniqueness of UCAN token. + + `iss`, Issuer, the DID of who sent this. + + `nbf`, Not Before, unix timestamp of when the jwt becomes valid. + + `prf`, Proof, an optional nested token with equal or greater privileges. + + ### Signature + + A signature (using `alg`) of the base64 encoded header and payload concatenated together and delimited by `.` + +## Installation ```elixir def deps do [ - {:ex_ucan, "~> 0.1.0"} + {:ex_ucan, git: "https://github.com/spawnfest/youcan.git"} ] end ``` Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) -and published on [HexDocs](https://hexdocs.pm). Once published, the docs can -be found at . +and published on [HexDocs](https://hexdocs.pm). + +## Usage + +### Generating UCAN + +- Create a Keypair + +- Use Ucan builder to build the payload + +- Sign the payload with the keypair + +- encode it to JWT format + +```elixir +iex> alias ExUcan.Builder + +# receiver DID +iex> audience_did = "did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2NLD" + +# Step 1: Create keypair +# default keypair generation uses EdDSA algorithm +iex> keypair = ExUcan.create_default_keypair() + +%ExUcan.Keymaterial.Ed25519.Keypair{ + jwt_alg: "EdDSA", + secret_key: <<119, 230, 103, 205, 6, 104, 32, 67, 206, 178, 128, 75, 16, + 177, 64, 44, 45, 238, 145, 226, 192, 163, 70, 36, 198, 1, 73, 61, 193, + 159, 100, 139>>, + public_key: <<253, 108, 63, 29, 71, 28, 139, 34, 170, 97, 117, 25, 179, + 124, 224, 206, 131, 150, 60, 212, 216, 168, 24, 85, 139, 119, 232, 14, + 64, 143, 2, 191>> +} +#################################################################### + +# Step 2: Use Ucan builder to build the payload +iex> ucan_payload = + Builder.default() + |> Builder.issued_by(keypair) + |> Builder.for_audience(audience_did) + |> Builder.with_lifetime(86_400) + |> Builder.build!() + +%ExUcan.Core.Structs.UcanPayload{ + ucv: "0.10.0", + iss: "did:key:z6MkmuTr3fgtBeTVmDtZZGmuHNrLwEA6b9KX4Shw1nyLioEy", + aud: "did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2NLD", + nbf: nil, + exp: 1698705462, + nnc: nil, + fct: %{}, + cap: [], + prf: [] +} +####################################################################### + +# Step 3: Sign the payload with the keypair (generated in step 1) +iex> ucan = ExUcan.sign(ucan_payload, keypair) + +%ExUcan.Core.Structs.Ucan{ + header: %ExUcan.Core.Structs.UcanHeader{ + alg: "EdDSA", + typ: "JWT" + }, + payload: %ExUcan.Core.Structs.UcanPayload{ + ucv: "0.10.0", + iss: "did:key:z6MkmuTr3fgtBeTVmDtZZGmuHNrLwEA6b9KX4Shw1nyLioEy", + aud: "did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2NLD", + nbf: nil, + exp: 1698705462, + nnc: nil, + fct: %{}, + cap: [], + prf: [] + }, + signed_data: "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTg3MDU0NjIsInVjdiI6IjAuMTAuMCIsImlzcyI6ImRpZDprZXk6ejZNa211VHIzZmd0QmVUVm1EdFpaR211SE5yTHdFQTZiOUtYNFNodzFueUxpb0V5IiwiYXVkIjoiZGlkOmtleTp6Nk1rd0RLM000UHhVMUZxY1N0NHF1WGdocXVIMU1vV1hHelRyTmtOV1RTeTJOTEQiLCJuYmYiOm51bGwsIm5uYyI6bnVsbCwiZmN0Ijp7fSwiY2FwIjpbXSwicHJmIjpbXX0", + signature: "aUwyis34wQBiPhDqaFjuRwUfSHhl1ZRJLwBlyqP2dKCY1syweuSPp1CY4zgMOE-iUFr8mug7CKqxuUKk8yzkBA" +} +############################################################################# + +# Step 4: encode it to JWT format +iex> ExUcan.encode(ucan) +"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTg3MDU0NjIsInVjdiI6IjAuMTAuMCIsImlzcyI6ImRpZDprZXk6ejZNa211VHIzZmd0QmVUVm1EdFpaR211SE5yTHdFQTZiOUtYNFNodzFueUxpb0V5IiwiYXVkIjoiZGlkOmtleTp6Nk1rd0RLM000UHhVMUZxY1N0NHF1WGdocXVIMU1vV1hHelRyTmtOV1RTeTJOTEQiLCJuYmYiOm51bGwsIm5uYyI6bnVsbCwiZmN0Ijp7fSwiY2FwIjpbXSwicHJmIjpbXX0.aUwyis34wQBiPhDqaFjuRwUfSHhl1ZRJLwBlyqP2dKCY1syweuSPp1CY4zgMOE-iUFr8mug7CKqxuUKk8yzkBA" +``` + +### Validating UCAN + +UCANs can be validated using + +``` +ExUcan.validate_token(token) +``` + +### Adding Capabilities + +`capabilities` are a list of `resources`, and the `abilities` that we can make on the `resource` with some optional `caveats`. + + +```elixir +cap = ExUcan.Core.Capability.new("example://bar", "ability/bar", %{"beep" => 1}) + +# where resource - example://bar", ability - "ability/bar" and caveat - %{"beep" => 1} +# This should be the only capability the receiver or `aud` of UCAN can do. We can add this capability in the ucan builder flow as + +iex> ucan_payload = + Builder.default() + |> Builder.issued_by(keypair) + |> Builder.for_audience(audience_did) + |> Builder.with_lifetime(86_400) + |> Builder.claiming_capability(cap) + |> Builder.build!() + +%ExUcan.Core.Structs.UcanPayload{ + ucv: "0.10.0", + iss: "did:key:z6MkmuTr3fgtBeTVmDtZZGmuHNrLwEA6b9KX4Shw1nyLioEy", + aud: "did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2NLD", + nbf: nil, + exp: 1698706505, + nnc: nil, + fct: %{}, + cap: [ + %ExUcan.Core.Capability{ + resource: "example://bar", + ability: "ability/bar", + caveat: %{"beep" => 1} + } + ], + prf: [] +} +``` + +## Roadmap + +The library is no-where feature parity with ucan [rust](https://github.com/ucan-wg/rs-ucan/tree/main) library or with the spec. The spec itself is nearing a 1.0.0, and is under-review. +But good thing is we have now laid the basic foundations. The next immediate additions would be, + +- [ ] Proof encodings as CID (Content Addressable Data) +- [ ] Delegation semantics +- [ ] Verifying UCAN invocations + + +## Acknowledgement + +- This library has taken reference from both [ts-ucan](https://github.com/ucan-wg/ts-ucan) and rs-ucan. + +- ExUcan logo - Validating ticket icons created by Good Ware - Flaticon diff --git a/doc_assets/ticket-4.png b/doc_assets/ticket-4.png new file mode 100644 index 0000000..95caaf6 Binary files /dev/null and b/doc_assets/ticket-4.png differ diff --git a/lib/ex_ucan/builder.ex b/lib/ex_ucan/builder.ex index 5ca2b76..ee53feb 100644 --- a/lib/ex_ucan/builder.ex +++ b/lib/ex_ucan/builder.ex @@ -116,19 +116,6 @@ defmodule ExUcan.Builder do %{builder | add_nonce?: true} end - # TODO: try to do this function - @doc """ - Includes a UCAN in the list of proofs for the UCAN to be built. - Note that the proof's audience must match this UCAN's issuer - or else the proof chain will be invalidated! - The proof is encoded into a [Cid], hashed via the [UcanBuilder::default_hasher()] - algorithm, unless one is provided. - """ - @spec witnessed_by(__MODULE__.t()) :: __MODULE__.t() - def witnessed_by(builder) do - builder - end - @doc """ Claim a capability by inheritance (from an authorizing proof) or implicitly by ownership of the resource by this UCAN's issuer @@ -138,10 +125,6 @@ defmodule ExUcan.Builder do %{builder | capabilities: builder.capabilities ++ [capability]} end - def delegating_from(builder) do - builder - end - @doc """ Builds the UCAN `payload` from the `Builder` workflow diff --git a/lib/ex_ucan/core/capability/data.ex b/lib/ex_ucan/core/capability/data.ex index 241e4f7..15fbd0e 100644 --- a/lib/ex_ucan/core/capability/data.ex +++ b/lib/ex_ucan/core/capability/data.ex @@ -1,5 +1,8 @@ defmodule ExUcan.Core.Capability do - # TODO: All the docs needed + @moduledoc """ + Capabilities are a list of `resources`, and the `abilities` that we + can make on the `resource` with some optional `caveats`. + """ @type t :: %__MODULE__{ resource: String.t(), ability: String.t(), @@ -7,6 +10,11 @@ defmodule ExUcan.Core.Capability do } defstruct [:resource, :ability, :caveat] + @doc """ + Creates a new capability with given resource, ability and caveat + + See `/test/capability_test.exs` + """ @spec new(String.t(), String.t(), list()) :: __MODULE__.t() def new(resource, ability, caveat) do %__MODULE__{ @@ -19,23 +27,19 @@ end defmodule ExUcan.Core.Capabilities do @moduledoc """ - Capabilities always deals with capabilites as map of maps - map> - """ - alias ExUcan.Core.Capability - # TODO: All the docs needed + Handling conversions of different type of group of capabilities - # def validate(capabilities) when is_map(capabilities) do - # capabilities - # |> Enum.reduce_while(%{}, fn {resource, ability}, caps -> - # # ability should be map - # # iter through ability + `Capabilities` are always maps of maps - # end) - # end + type reference - map> + """ + alias ExUcan.Core.Capability - def validate(_), do: {:error, "Capabilities must be an object."} + @doc """ + Convert capabilites represented in maps to list of capabilites + See `/test/capability_test.exs` + """ @spec map_to_sequence(map()) :: list(Capability.t()) def map_to_sequence(capabilities) do capabilities @@ -45,6 +49,11 @@ defmodule ExUcan.Core.Capabilities do end) end + @doc """ + Convert capabilites represented as list of capabilities to maps of maps + + See `/test/capability_test.exs` + """ @spec sequence_to_map(list(Capability.t())) :: map() def sequence_to_map(capabilites) do capabilites diff --git a/lib/ex_ucan/core/structs.ex b/lib/ex_ucan/core/structs.ex index 9e94b7c..3738a3d 100644 --- a/lib/ex_ucan/core/structs.ex +++ b/lib/ex_ucan/core/structs.ex @@ -1,6 +1,11 @@ defmodule ExUcan.Core.Structs.UcanHeader do @moduledoc """ - Ucan header + Ucan header representation + """ + + @typedoc """ + alg - Algorithm used (ex EdDSA) + typ - Type of token format (ex JWT) """ @type t :: %__MODULE__{ alg: String.t(), @@ -13,10 +18,23 @@ end defmodule ExUcan.Core.Structs.UcanPayload do @moduledoc """ - Ucan Payload + Ucan Payload representation """ alias ExUcan.Core.Capability + @typedoc """ + + ucv: UCAN version. + iss: Issuer, the DID of who sent this. + aud: Audience, the DID of who it's intended for. + nbf: Not Before, unix timestamp of when the jwt becomes valid. + exp: Expiry, unix timestamp of when the jwt is no longer valid. + nnc: Nonce value to increase the uniqueness of UCAN token. + fct: Facts, an array of extra facts or information to attach to the jwt. + cap: A list of resources and capabilities that the ucan grants. + prf: Proof, an optional nested token with equal or greater privileges. + + """ @type t :: %__MODULE__{ ucv: String.t(), iss: String.t(), @@ -40,6 +58,12 @@ defmodule ExUcan.Core.Structs.Ucan do alias ExUcan.Core.Structs.UcanHeader alias ExUcan.Core.Structs.UcanPayload + @typedoc """ + header - Token Header + payload - Token payload + signed_data - Data that would be eventually signed + signature - Base64Url encoded signature + """ @type t :: %__MODULE__{ header: UcanHeader.t(), payload: UcanPayload.t(), diff --git a/lib/ex_ucan/core/token.ex b/lib/ex_ucan/core/token.ex index 3e8cc94..6d5d21c 100644 --- a/lib/ex_ucan/core/token.ex +++ b/lib/ex_ucan/core/token.ex @@ -1,6 +1,6 @@ defmodule ExUcan.Core.Token do @moduledoc """ - Creates and manages UCAN tokens + Core functions for the creation and management of UCAN tokens """ alias ExUcan.Builder alias ExUcan.Core.Structs.Ucan diff --git a/test/capability_test.exs b/test/capability_test.exs index 5aa7841..aa72cf3 100644 --- a/test/capability_test.exs +++ b/test/capability_test.exs @@ -14,29 +14,4 @@ defmodule CapabilityTest do cap_maps = Capabilities.sequence_to_map(cap_sequence) assert Capabilities.map_to_sequence(cap_maps) == cap_sequence end - - test "it_rejects_non_compliant_json" do - failure_cases = [ - { - [], - "resources must be map" - }, - { - %{"resource:foo" => []}, - "abilities must be map" - }, - { - %{"resource:foo" => {}}, - "resource must have at least one ability" - }, - { - %{"resource:foo" => %{"ability/read" => %{}}}, - "caveats must be a list" - }, - { - %{"resource:foo" => %{"ability/read" => [1]}}, - "caveat must be object" - } - ] - end end