Skip to content

Commit

Permalink
siopv2 authorize requests
Browse files Browse the repository at this point in the history
  • Loading branch information
patatoid committed Jan 27, 2024
1 parent b52bb23 commit 25c8a60
Show file tree
Hide file tree
Showing 22 changed files with 512 additions and 32 deletions.
1 change: 1 addition & 0 deletions lib/boruta/adapters/clients.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule Boruta.ClientsAdapter do
import Boruta.Config, only: [clients: 0]

def get_client(id), do: clients().get_client(id)
def public!, do: clients().public!()
def authorized_scopes(params), do: clients().authorized_scopes(params)
def list_clients_jwk, do: clients().list_clients_jwk()
def create_client(registration_params), do: clients().create_client(registration_params)
Expand Down
19 changes: 18 additions & 1 deletion lib/boruta/adapters/ecto/clients.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Boruta.Ecto.Clients do
@behaviour Boruta.Oauth.Clients
@behaviour Boruta.Openid.Clients

import Boruta.Config, only: [repo: 0]
import Boruta.Config, only: [repo: 0, issuer: 0]
import Boruta.Ecto.OauthMapper, only: [to_oauth_schema: 1]

alias Boruta.Ecto
Expand All @@ -28,6 +28,23 @@ defmodule Boruta.Ecto.Clients do
end
end

@impl Boruta.Oauth.Clients
def public! do
case public!(:from_cache) do
{:ok, client} -> client
{:error, _reason} -> public!(:from_database)
end
end

defp public!(:from_cache), do: ClientStore.get_public()

defp public!(:from_database) do
with %Ecto.Client{} = client <- repo().get_by(Ecto.Client, public_client_id: issuer()),
{:ok, client} <- client |> to_oauth_schema() |> ClientStore.put_public() do
client
end
end

def invalidate(client) do
ClientStore.invalidate(client)
end
Expand Down
3 changes: 1 addition & 2 deletions lib/boruta/adapters/ecto/codes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ defmodule Boruta.Ecto.Codes do
id: client_id,
authorization_code_ttl: authorization_code_ttl
} = client,
resource_owner: resource_owner,
redirect_uri: redirect_uri,
scope: scope,
state: state,
Expand All @@ -78,7 +77,7 @@ defmodule Boruta.Ecto.Codes do

changeset =
apply(Token, changeset_method(client), [
%Token{resource_owner: resource_owner},
%Token{resource_owner: params[:resource_owner]},
%{
client_id: client_id,
sub: sub,
Expand Down
26 changes: 16 additions & 10 deletions lib/boruta/adapters/ecto/oauth_mapper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,22 @@ defimpl Boruta.Ecto.OauthMapper, for: Boruta.Ecto.Token do
alias Boruta.Ecto.OauthMapper

def to_oauth_schema(%Ecto.Token{} = token) do
client = case Clients.get_client(token.client_id) do
%Oauth.Client{} = client -> client
_ -> nil
end
resource_owner = token.resource_owner || with "" <> sub <- token.sub, # token is linked to a resource_owner
{:ok, resource_owner} <- resource_owners().get_by(sub: sub) do
resource_owner
else
_ -> nil
end
client =
case Clients.get_client(token.client_id) do
%Oauth.Client{} = client -> client
_ -> nil
end

# token is linked to a resource_owner
resource_owner =
token.resource_owner ||
with "" <> sub <- token.sub,
false <- Regex.match?(~r/did:/, sub),
{:ok, resource_owner} <- resource_owners().get_by(sub: sub) do
resource_owner
else
_ -> nil
end

struct(
Oauth.Token,
Expand Down
1 change: 1 addition & 0 deletions lib/boruta/adapters/ecto/schemas/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ defmodule Boruta.Ecto.Client do
@foreign_key_type :binary_id
@timestamps_opts type: :utc_datetime
schema "oauth_clients" do
field(:public_client_id, :string)
field(:name, :string)
field(:secret, :string)
field(:confidential, :boolean, default: false)
Expand Down
16 changes: 16 additions & 0 deletions lib/boruta/adapters/ecto/stores/client_store.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ defmodule Boruta.Ecto.ClientStore do
cache_backend().get({Client, id})
end

@spec get_public() :: client :: Boruta.Oauth.Client.t()
def get_public do
case cache_backend().get({Client, :public}) do
nil -> {:error, "No public client stored."}
client -> {:ok, client}
end
end

def authorized_scopes(%Client{id: id}) do
case get_by_id(id) do
nil -> {:error, "Client not cached."}
Expand All @@ -33,6 +41,14 @@ defmodule Boruta.Ecto.ClientStore do
end
end

@spec put_public(client :: Boruta.Oauth.Client.t()) ::
{:ok, client :: Boruta.Oauth.Client.t()} | {:error, reason :: String.t()}
def put_public(client) do
with :ok <- cache_backend().put({Client, :public}, client) do
{:ok, client}
end
end

@spec invalidate(client :: %Client{}) :: :ok
def invalidate(client) do
cache_backend().delete({Client, client.id})
Expand Down
7 changes: 7 additions & 0 deletions lib/boruta/oauth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ defmodule Boruta.Oauth do
alias Boruta.Oauth.Revoke
alias Boruta.Oauth.TokenResponse
alias Boruta.Openid.CredentialOfferResponse
alias Boruta.Openid.SiopV2Response

@doc """
Process an token request as stated in [RFC 6749 - The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749).
Expand Down Expand Up @@ -105,6 +106,12 @@ defmodule Boruta.Oauth do
response
)

%SiopV2Response{} = response ->
module.authorize_success(
conn,
response
)

%CredentialOfferResponse{} = response ->
module.authorize_success(
conn,
Expand Down
101 changes: 99 additions & 2 deletions lib/boruta/oauth/authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ defmodule Boruta.Oauth.AuthorizationSuccess do
code: nil,
code_challenge: nil,
code_challenge_method: nil,
authorization_details: nil
authorization_details: nil,
issuer: nil

@type t :: %__MODULE__{
response_types: list(String.t()),
Expand All @@ -58,7 +59,8 @@ defmodule Boruta.Oauth.AuthorizationSuccess do
nonce: String.t() | nil,
code_challenge: String.t() | nil,
code_challenge_method: String.t() | nil,
authorization_details: list(map()) | nil
authorization_details: list(map()) | nil,
issuer: String.t() | nil
}
end

Expand Down Expand Up @@ -630,6 +632,101 @@ defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.CodeRequest do
defp check_code_challenge(%Client{pkce: true}, _code_challenge, _code_challenge_method), do: :ok
end

defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.SiopV2Request do
alias Boruta.ClientsAdapter
alias Boruta.CodesAdapter
alias Boruta.Oauth.Authorization
alias Boruta.Oauth.AuthorizationSuccess
alias Boruta.Oauth.CodeRequest
alias Boruta.Oauth.Error
alias Boruta.Oauth.SiopV2Request
alias Boruta.Oauth.Token
alias Boruta.VerifiableCredentials

def preauthorize(
%SiopV2Request{
client_id: sub,
redirect_uri: redirect_uri,
state: state,
nonce: nonce,
scope: scope,
code_challenge: code_challenge,
code_challenge_method: code_challenge_method,
authorization_details: authorization_details,
client_metadata: client_metadata
} = request
) do
client = ClientsAdapter.public!()

with {:ok, scope} <-
Authorization.Scope.authorize(
scope: scope,
against: %{client: client}
),
:ok <- Authorization.Nonce.authorize(request),
:ok <- VerifiableCredentials.validate_authorization_details(authorization_details),
:ok <- check_client_metadata(client_metadata) do
{:ok,
%AuthorizationSuccess{
redirect_uri: redirect_uri,
client: client,
sub: sub,
scope: scope,
state: state,
nonce: nonce,
code_challenge: code_challenge,
code_challenge_method: code_challenge_method,
authorization_details: Jason.decode!(authorization_details)
}}
else
{:error, :invalid_code_challenge} ->
{:error,
%Error{
status: :bad_request,
error: :invalid_request,
error_description: "Code challenge is invalid."
}}

error ->
error
end
end

def token(request) do
with {:ok,
%AuthorizationSuccess{
redirect_uri: redirect_uri,
client: client,
sub: sub,
scope: scope,
state: state,
nonce: nonce,
code_challenge: code_challenge,
code_challenge_method: code_challenge_method,
authorization_details: authorization_details
}} <-
preauthorize(request) do
with {:ok, code} <-
CodesAdapter.create(%{
client: client,
redirect_uri: redirect_uri,
sub: sub,
scope: scope,
state: state,
nonce: nonce,
code_challenge: code_challenge,
code_challenge_method: code_challenge_method,
authorization_details: authorization_details
}) do
{:ok, %{siopv2_code: code}}
end
end
end

# TODO perform client metadata checks
defp check_client_metadata(_client_metadata), do: :ok
end

defimpl Boruta.Oauth.Authorization, for: Boruta.Oauth.HybridRequest do
alias Boruta.AccessTokensAdapter
alias Boruta.CodesAdapter
Expand Down
19 changes: 19 additions & 0 deletions lib/boruta/oauth/authorization/nonce.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,25 @@ defprotocol Boruta.Oauth.Authorization.Nonce do
def authorize(request)
end

defimpl Boruta.Oauth.Authorization.Nonce, for: Boruta.Oauth.SiopV2Request do
alias Boruta.Oauth.Error

def authorize(%Boruta.Oauth.SiopV2Request{nonce: nonce}) do
case nonce do
nonce when nonce in [nil, ""] ->
{:error,
%Error{
status: :bad_request,
error: :invalid_request,
error_description: "OpenID requests require a nonce."
}}

_ ->
:ok
end
end
end

defimpl Boruta.Oauth.Authorization.Nonce, for: Boruta.Oauth.CodeRequest do
alias Boruta.Oauth.CodeRequest
alias Boruta.Oauth.Error
Expand Down
5 changes: 5 additions & 0 deletions lib/boruta/oauth/contexts/clients.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ defmodule Boruta.Oauth.Clients do
"""
@callback get_client(id :: any()) :: client :: Boruta.Oauth.Client.t() | nil

@doc """
Returns the public `Boruta.Oauth.Client`.
"""
@callback public!() :: client :: Boruta.Oauth.Client.t() | nil

@doc """
Returns client authorized scopes. The scopes will be granted for every requests to the given client.
"""
Expand Down
4 changes: 2 additions & 2 deletions lib/boruta/oauth/contexts/codes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ defmodule Boruta.Oauth.Codes do
:state => String.t(),
:code_challenge => String.t(),
:code_challenge_method => String.t(),
:resource_owner => Boruta.Oauth.ResourceOwner.t(),
:authorization_details => list(map())
:authorization_details => list(map()),
optional(:resource_owner) => Boruta.Oauth.ResourceOwner.t()
}) :: code :: Boruta.Oauth.Token.t() | {:error, reason :: term()}

@doc """
Expand Down
35 changes: 27 additions & 8 deletions lib/boruta/oauth/error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule Boruta.Oauth.Error do
alias Boruta.Oauth.Error
alias Boruta.Oauth.HybridRequest
alias Boruta.Oauth.PreauthorizedCodeRequest
alias Boruta.Oauth.SiopV2Request
alias Boruta.Oauth.TokenRequest

@type t :: %__MODULE__{
Expand Down Expand Up @@ -41,17 +42,25 @@ defmodule Boruta.Oauth.Error do
"""
@spec with_format(
error :: Error.t(),
request :: CodeRequest.t() | TokenRequest.t() | HybridRequest.t() | PreauthorizedCodeRequest.t()
request ::
CodeRequest.t()
| TokenRequest.t()
| HybridRequest.t()
| PreauthorizedCodeRequest.t()
| SiopV2Request.t()
) :: Error.t()
def with_format(%Error{error: :invalid_client} = error, _) do
%{error | format: nil, redirect_uri: nil}
end

def with_format(%Error{error: :invalid_resource_owner} = error, %HybridRequest{
redirect_uri: redirect_uri,
state: state,
prompt: "none"
} = request) do
def with_format(
%Error{error: :invalid_resource_owner} = error,
%HybridRequest{
redirect_uri: redirect_uri,
state: state,
prompt: "none"
} = request
) do
%{
error
| error: :login_required,
Expand Down Expand Up @@ -96,15 +105,25 @@ defmodule Boruta.Oauth.Error do
%{error | format: :query, redirect_uri: redirect_uri, state: state}
end

def with_format(%Error{} = error, %HybridRequest{redirect_uri: redirect_uri, state: state} = request) do
def with_format(%Error{} = error, %SiopV2Request{redirect_uri: redirect_uri, state: state}) do
%{error | format: :query, redirect_uri: redirect_uri, state: state}
end

def with_format(
%Error{} = error,
%HybridRequest{redirect_uri: redirect_uri, state: state} = request
) do
%{error | format: response_mode(request), redirect_uri: redirect_uri, state: state}
end

def with_format(%Error{} = error, %TokenRequest{redirect_uri: redirect_uri, state: state}) do
%{error | format: :fragment, redirect_uri: redirect_uri, state: state}
end

def with_format(%Error{} = error, %PreauthorizedCodeRequest{redirect_uri: redirect_uri, state: state}) do
def with_format(%Error{} = error, %PreauthorizedCodeRequest{
redirect_uri: redirect_uri,
state: state
}) do
%{error | format: :fragment, redirect_uri: redirect_uri, state: state}
end

Expand Down
3 changes: 1 addition & 2 deletions lib/boruta/oauth/json/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,7 @@ defmodule Boruta.Oauth.Json.Schema do
"response_type" => %{"type" => "string", "pattern" => "code"},
"response_mode" => %{"type" => "string", "pattern" => "^(query|fragment)$"},
"client_id" => %{
"type" => "string",
"pattern" => @uuid_pattern
"type" => "string"
},
"state" => %{"type" => "string"},
"nonce" => %{"type" => "string"},
Expand Down
Loading

0 comments on commit 25c8a60

Please sign in to comment.