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 """
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