Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
oid4vci credential request
Browse files Browse the repository at this point in the history
patatoid committed Jan 1, 2024
1 parent 40d4a56 commit 0f0f61f
Showing 9 changed files with 261 additions and 2 deletions.
77 changes: 75 additions & 2 deletions lib/boruta/openid.ex
Original file line number Diff line number Diff line change
@@ -2,6 +2,16 @@ defmodule Boruta.OpenidModule do
@moduledoc false
@callback jwks(conn :: Plug.Conn.t() | map(), module :: atom()) :: any()
@callback userinfo(conn :: Plug.Conn.t() | map(), module :: atom()) :: any()
@callback register_client(
conn :: Plug.Conn.t() | map(),
registration_params :: map(),
module :: atom()
) :: any()
@callback credential(
conn :: Plug.Conn.t() | map(),
credential_params :: map(),
module :: atom()
) :: any()
end

defmodule Boruta.Openid do
@@ -16,7 +26,9 @@ defmodule Boruta.Openid do
alias Boruta.ClientsAdapter
alias Boruta.Oauth.Authorization.AccessToken
alias Boruta.Oauth.BearerToken
alias Boruta.Oauth.Error
alias Boruta.Oauth.Token
alias Boruta.Openid.CredentialResponse
alias Boruta.Openid.UserinfoResponse

def jwks(conn, module) do
@@ -48,8 +60,69 @@ defmodule Boruta.Openid do
end
end

def credential, do: :not_implemented
def credential_offer, do: :not_implemented
def credential(conn, credential_params, module) do
with {:ok, access_token} <- BearerToken.extract_token(conn),
{:ok, token} <- AccessToken.authorize(value: access_token),
{:ok, credential_params} <- validate_credential_params(credential_params),
:ok <- validate_credential_identifier(token, credential_params) do

response = CredentialResponse.from_tokens(%{access_token: token}, credential_params)
module.credential_created(conn, response)
else
{:error, %Error{} = error} ->
module.credential_failure(conn, error)

{:error, reason} ->
error = %Error{
status: :bad_request,
error: :invalid_request,
error_description: reason
}

module.credential_failure(conn, error)
end

# TODO validate JWT proof
# TODO verify the proof https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-verifying-key-proof
# TODO credential response
end

alias Boruta.Openid.Json.Schema
alias ExJsonSchema.Validator.Error.BorutaFormatter

defp validate_credential_params(params) do
case ExJsonSchema.Validator.validate(
Schema.credential(),
params,
error_formatter: BorutaFormatter
) do
:ok ->
{:ok, params}

{:error, errors} ->
{:error, "Request body validation failed. " <> Enum.join(errors, " ")}
end
end

defp validate_credential_identifier(
%Token{authorization_details: authorization_details},
%{"credential_identifier" => credential_identifier}
) do
case authorization_details
|> Enum.flat_map(fn %{"credential_identifiers" => credential_identifiers} ->
credential_identifiers
end)
|> Enum.member?(credential_identifier) do
true ->
:ok

false ->
{:error, "Invalid credential identifier."}
end
end

defp validate_credential_identifier(_token, _credential_identifier),
do: {:error, "Invalid credential identifier."}

defp parse_registration_params(params, %{jwks: %{"keys" => [jwk]}} = acc) do
params =
10 changes: 10 additions & 0 deletions lib/boruta/openid/application.ex
Original file line number Diff line number Diff line change
@@ -31,4 +31,14 @@ defmodule Boruta.Openid.Application do
"""
@callback registration_failure(conn :: Plug.Conn.t(), changeset :: Ecto.Changeset.t()) ::
any()
@doc """
This function will be triggered in case of success invoking `Boruta.Openid.credential/3`
"""
@callback credential_created(conn :: Plug.Conn.t(), credential :: Boruta.Openid.CredentialResponse.t()) ::
any()
@doc """
This function will be triggered in case of failure invoking `Boruta.Openid.credential/3`
"""
@callback credential_failure(conn :: Plug.Conn.t(), error :: Boruta.Oauth.Error.t()) ::
any()
end
17 changes: 17 additions & 0 deletions lib/boruta/openid/applications/credential_application.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule Boruta.Openid.CredentialApplication do
@moduledoc """
Implement this behaviour in the application layer of your OpenID Connect provider.
This behaviour gives all callbacks triggered invoking `Boruta.Openid.credential/3` function.
"""

@doc """
This function will be triggered in case of success invoking `Boruta.Openid.credential/3`
"""
@callback credential_created(conn :: Plug.Conn.t(), credential :: Boruta.Openid.CredentialResponse.t()) ::
any()
@doc """
This function will be triggered in case of failure invoking `Boruta.Openid.credential/3`
"""
@callback credential_failure(conn :: Plug.Conn.t(), error :: Boruta.Oauth.Error.t()) ::
any()
end
26 changes: 26 additions & 0 deletions lib/boruta/openid/json/schema.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule Boruta.Openid.Json.Schema do
@moduledoc false
alias ExJsonSchema.Schema

@uuid_pattern "\^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}\$"

def credential do
%{
"type" => "object",
"properties" => %{
"format" => %{"type" => "string"},
"proof" => %{
"type" => "object",
"properties" => %{
"proof_type" => %{"type" => "string", "pattern" => "^jwt$"},
"jwt" => %{"type" => "string"},
},
"required" => ["proof_type", "jwt"]
},
"credential_identifier" => %{"type" => "string"},
},
"required" => ["credential_identifier"]
}
|> Schema.resolve()
end
end
23 changes: 23 additions & 0 deletions lib/boruta/openid/responses/credential.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Boruta.Openid.CredentialResponse do
@moduledoc """
Response in case of delivrance of verifiable credential
"""

@enforce_keys [:format, :credential]
defstruct format: nil,
credential: nil

@type t :: %__MODULE__{
format: String.t(),
credential: String.t()
}

def from_tokens(%{
access_token: _access_token
}, _credential_params) do
%__MODULE__{
format: "jwt_vc_json",
credential: ""
}
end
end
File renamed without changes.
104 changes: 104 additions & 0 deletions test/boruta/openid/integration/credential_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
defmodule Boruta.OpenidTest.CredentialTest do
use Boruta.DataCase

import Boruta.Factory
import Plug.Conn

alias Boruta.Ecto.Token
alias Boruta.Oauth.Error
alias Boruta.Openid
alias Boruta.Openid.ApplicationMock
alias Boruta.Openid.CredentialResponse

describe "deliver verifiable credentials" do
test "returns an error with no access token" do
conn = %Plug.Conn{}

assert {:credential_failure,
%Boruta.Oauth.Error{
error: :invalid_request,
error_description: "Invalid bearer from Authorization header.",
status: :bad_request
}} = Openid.credential(conn, %{}, ApplicationMock)
end

test "returns credential_failure with a bad authorization header" do
conn =
%Plug.Conn{}
|> put_req_header("authorization", "not a bearer")

assert {:credential_failure,
%Boruta.Oauth.Error{
error: :invalid_request,
error_description: "Invalid bearer from Authorization header.",
status: :bad_request
}} = Openid.credential(conn, %{}, ApplicationMock)
end

test "returns credential_failure with a bad access token" do
conn =
%Plug.Conn{}
|> put_req_header("authorization", "Bearer bad_token")

assert {:credential_failure,
%Boruta.Oauth.Error{
error: :invalid_access_token,
error_description: "Given access token is invalid, revoked, or expired.",
status: :bad_request
}} = Openid.credential(conn, %{}, ApplicationMock)
end

test "returns an error with a valid bearer" do
credential_params = %{}
%Token{value: access_token} = insert(:token)

conn =
%Plug.Conn{}
|> put_req_header("authorization", "Bearer #{access_token}")

assert Openid.credential(conn, credential_params, ApplicationMock) ==
{:credential_failure,
%Error{
status: :bad_request,
error: :invalid_request,
error_description:
"Request body validation failed. Required property credential_identifier is missing at #."
}}
end

test "returns an error with an invalid credential_identifier" do
credential_params = %{"credential_identifier" => "bad identifier"}
%Token{value: access_token} = insert(:token)

conn =
%Plug.Conn{}
|> put_req_header("authorization", "Bearer #{access_token}")

assert Openid.credential(conn, credential_params, ApplicationMock) ==
{:credential_failure,
%Error{
status: :bad_request,
error: :invalid_request,
error_description: "Invalid credential identifier."
}}
end

test "returns a credential with a valid credential_identifier" do
credential_params = %{"credential_identifier" => "identifier"}

%Token{value: access_token} =
insert(:token, authorization_details: [%{"credential_identifiers" => ["identifier"]}])

conn =
%Plug.Conn{}
|> put_req_header("authorization", "Bearer #{access_token}")

assert Openid.credential(conn, credential_params, ApplicationMock) ==
{:credential_created,
%CredentialResponse{
format: "jwt_vc_json",
credential: ""
}}
end
end
end
6 changes: 6 additions & 0 deletions test/support/boruta/openid/application_mock.ex
Original file line number Diff line number Diff line change
@@ -16,4 +16,10 @@ defmodule Boruta.Openid.ApplicationMock do

@impl Boruta.Openid.Application
def registration_failure(_conn, changeset), do: {:registration_failure, changeset}

@impl Boruta.Openid.Application
def credential_created(_conn, credential), do: {:credential_created, credential}

@impl Boruta.Openid.Application
def credential_failure(_conn, error), do: {:credential_failure, error}
end

0 comments on commit 0f0f61f

Please sign in to comment.