defmodule Magic.Token do @moduledoc """ Provides methods to interact with the DID Token. """ @nbf_grace_period 300 @typedoc """ A DID Token generated by a Magic user on the client-side. """ @type did_token :: String.t() @typedoc """ A map of claims made by the DID Token """ @type claim :: %{ add: String.t(), aud: String.t(), ext: integer, iat: integer, iss: String.t(), nbf: integer, sub: String.t(), tid: String.t() } @typedoc """ Type of the crypto wallet of the Magic User """ @type wallet_type :: :eth | :harmony | :icon | :flow | :tezos | :zilliqa | :polkadot | :solana | :avax | :algod | :cosmos | :celo | :bitcoin | :near | :helium | :conflux | :terra | :taquito | :ed | :hedera | :none | :any @typedoc """ Cryptographic public address of the Magic User. """ @type public_address :: String.t() @typedoc """ Decentralized identifier which encapsulates a unique user ID """ @type issuer :: String.t() alias Magic.Utils alias Magic.DIDTokenError @doc """ Validates did_token Returns true or raises an error """ @spec validate!(did_token) :: true def validate!(did_token) do time = DateTime.utc_now() |> DateTime.to_unix() %{proof: proof, claim: claim, message: message} = decode!(did_token) rec_address = recover_address(proof, message) validate_public_address!(rec_address, did_token) validate_claim_fields!(claim) validate_claim_ext!(time, claim["ext"]) validate_claim_nbf!(time, claim["nbf"]) true end @doc """ Validates did_token Returns :ok or an error tuple """ @spec validate(did_token) :: :ok | {:error, {:did_token_error, String.t()}} def validate(did_token) do try do validate!(did_token) :ok rescue e in DIDTokenError -> {:error, {:did_token_error, e.message}} end end @doc """ Decodes a DID Token from a Base64 string into a tuple of its individual components: proof and claim. This method allows you decode the DID Token and inspect the token Returns A map containing proof, claim and message or raise an error """ @spec decode!(did_token) :: %{proof: String.t(), claim: claim, message: String.t()} def decode!(did_token) do try do [proof, message] = Base.decode64!(did_token) |> Jason.decode!() claim = Jason.decode!(message) validate_claim_fields!(claim) %{proof: proof, claim: claim, message: message} rescue ArgumentError -> raise DIDTokenError, message: "DID Token is malformed" Jason.DecodeError -> raise DIDTokenError, message: "DID Token is malformed" end end @doc """ Decodes a DID Token from a Base64 string into a tuple of its individual components: proof and claim. This method allows you decode the DID Token and inspect the token Returns an :ok tuple with a map containing proof, claim and message or an error tuple """ @spec decode(did_token) :: {:ok, %{proof: String.t(), claim: claim, message: String.t()}} | {:error, {:did_token_error, String.t()}} def decode(did_token) do try do decoded = decode!(did_token) {:ok, decoded} rescue e in DIDTokenError -> {:error, {:did_token_error, e.message}} end end @doc """ Parses public_address and extracts issuer Returns issuer info """ @spec construct_issuer_with_public_address(public_address) :: issuer def construct_issuer_with_public_address(public_address) do "did:ethr:#{public_address}" end @doc """ Parses did_token and extracts issuer Returns issuer info """ @spec get_issuer(did_token) :: issuer def get_issuer(did_token) do %{claim: claim} = decode!(did_token) claim["iss"] end @doc """ Parses did_token and extracts cryptographic public_address Returns cryptographic public address of the Magic User who generated the supplied DID Token. """ @spec get_public_address(did_token) :: public_address def get_public_address(did_token) do get_issuer(did_token) |> String.split(":") |> List.last() end defp recover_address(proof, message) do Utils.recover_pubkey(message, proof) |> Utils.pubkey_to_address() end defp claim_fields() do ~w(iat ext iss sub aud nbf tid) end defp validate_claim_fields!(claim) do missing_fields = claim_fields() -- Map.keys(claim) if length(missing_fields) > 0 do raise DIDTokenError, message: "DID Token missing required fields: #{Enum.join(missing_fields, ", ")}" end end defp validate_public_address!(rec_address, did_token) do if Utils.hex_to_bin(rec_address) != Utils.hex_to_bin(get_public_address(did_token)) do message = "Signature mismatch between 'proof' and 'claim'." raise DIDTokenError, message: message end end defp validate_claim_ext!(time, claim_ext) do if time > claim_ext do message = "Given DID token has expired. Please generate a new one." raise DIDTokenError, message: message end end defp apply_nbf_grace_period(claim_nbf) do claim_nbf - @nbf_grace_period end defp validate_claim_nbf!(time, claim_nbf) do if time < apply_nbf_grace_period(claim_nbf) do message = "Given DID token cannot be used at this time." raise DIDTokenError, message: message end end end