Skip to content

Commit

Permalink
Merge branch 'main' of github.com:tobiasbernet/schlusseli into main
Browse files Browse the repository at this point in the history
  • Loading branch information
tobiasbernet committed Sep 2, 2021
2 parents d5fdefe + 4d10449 commit fdfd1cc
Show file tree
Hide file tree
Showing 15 changed files with 477 additions and 59 deletions.
3 changes: 1 addition & 2 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
[
import_deps: [:ecto, :phoenix],
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
subdirectories: ["priv/*/migrations"]
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Schlusseli

A very simple OAuth example using Keycloak.

To start your Phoenix server:

* Install dependencies with `mix deps.get`
Expand All @@ -17,13 +19,60 @@ Create 3 clients:

* `schlusseli-api`: Bearer-only client. The API backend service.
* `schlusseli-ui`: Confidential client has access to the `schlusseli-api` service.
* `evil-ui`: Confidential client, has no access to the `schlusseli-api`.
* `evil-ui`: Confidential client, has no access to the `schlusseli-api`.

Setup an audience:

* [Keycloak Audience](https://www.keycloak.org/docs/latest/server_admin/#_audience)
* The audience of the client token `schlusseli-ui` must include `schlusseli-api`.
* The app gives only access to clients with the correct audience.

### Bearer-only
The API (`schlusseli`) has no knowledge about the user authentication process. The application only allows bearer token requests.

`Schlusseli` verifies the given JWT Token and gives access to the client.

## Elixir configurations

### Schlusseli.Plug
I added two different plugs to `schlusseli` to play around with the different token verification processes:

* `Schlusseli.Plug.KeycloakIntorspect`: Uses Keycloak OpenID introspect endpoint to verify the token.
* `Schlusseli.Plug.OpenidConnector`: Uses the [OpenIDConnect](https://hexdocs.pm/openid_connect/readme.html) hex lib to verify the token against the public key.


### SchlusseliWeb.Schema.Middleware.Authorize
A very simple Middleware to check if the client (`schlusseli-ui`) has the correct scope to access the schema.

Only clients with a `view_customers` scope have access to view all customers.
```Elixir
field :customers, list_of(:customer) do
middleware(SchlusseliWeb.Schema.Middleware.Authorize, "view_customers")
resolve(&Resolvers.Customer.list_customers/3)
end
```

## Postman

Use postman for authorizing requests: https://learning.postman.com/docs/sending-requests/authorization/

![alt text](docs/img/postman_auth.png "Postman")

## Learn more

**Phoenix**
* Official website: http://www.phoenixframework.org/
* Guides: https://hexdocs.pm/phoenix/overview.html
* Docs: https://hexdocs.pm/phoenix
* Mailing list: http://groups.google.com/group/phoenix-talk
* Source: https://github.com/phoenixframework/phoenix

**Keycloak**
* Server Admin: https://www.keycloak.org/docs/latest/server_admin/
* Guide: https://www.keycloak.org/docs/latest/getting_started/

**Useful links**
* JSON Web Key Set Properties: https://auth0.com/docs/security/tokens/json-web-tokens/json-web-key-set-properties
* JWT Debugger: https://jwt.io/
* Secure your Microservices with Keycloak: [2019-NN-Sebastien_Blanc-Easily_Secure_your_Microservices_with_Keycloak-Praesentation.pdf](https://www.doag.org/formes/pubfiles/11143470/2019-NN-Sebastien_Blanc-Easily_Secure_your_Microservices_with_Keycloak-Praesentation.pdf)
#
8 changes: 4 additions & 4 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ config :phoenix, :json_library, Jason
# OpenID provider config
config :schlusseli, :openid_connect_providers,
keycloak: [
discovery_document_uri: "http://127.0.0.1:8085/auth/realms/Schlusseli/.well-known/openid-configuration",
introspect_uri: "http://127.0.0.1:8085/auth/realms/Schlusseli/protocol/openid-connect/token/introspect",
discovery_document_uri:
"http://127.0.0.1:8085/auth/realms/Schlusseli/.well-known/openid-configuration",
introspect_uri:
"http://127.0.0.1:8085/auth/realms/Schlusseli/protocol/openid-connect/token/introspect",
client_id: "schlusseli-api",
client_secret: "9b81d2f0-1f5d-4a12-8d3a-3032be945c5a",
redirect_uri: "",
Expand All @@ -38,8 +40,6 @@ config :schlusseli, :openid_connect_providers,
verify_token_audience: true
]



# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
Binary file added docs/img/postman_auth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions lib/schlusseli/factory.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ defmodule Schlusseli.Factory do

def key_factory() do
%{
id: sequence(:id, &(&1), start_at: 1),
id: sequence(:id, & &1, start_at: 1),
serial: :os.system_time(:millisecond),
type: sequence(:type, @key_types)
}
end

def customer_factory do
%{
id: sequence(:id, &(&1), start_at: 1),
id: sequence(:id, & &1, start_at: 1),
name: "Hans Lock Smith",
email: sequence(:email, &"email-#{&1}@example.com")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
defmodule Schlusseli.Plug.KeycloakCustomConnector do
@moduledoc """
Plug for verifying authorization on a per request basis, verifies that a token is set in the
`Authorization` header.
defmodule Schlusseli.Plug.KeycloakIntorspect do
@moduledoc """
Plug to verify token via keycloak's introspection endpoint.
Problem: OpenIDConnect verifies the token with the public key. No `introspect` is made. We don't know if the token is valid or not.
The verification by the Keycloak introspect endpoint ensures that the token becomes invalid after a revoke.
"""

import Plug.Conn
Expand All @@ -19,9 +19,9 @@ defmodule Schlusseli.Plug.KeycloakCustomConnector do
@spec call(Plug.Conn.t(), keyword()) :: Plug.Conn.t()
def call(conn, _) do
conn
|> get_req_header("authorization")
|> fetch_token()
|> verify_token(conn)
|> get_req_header("authorization")
|> fetch_token()
|> verify_token(conn)
end

def fetch_token([token]) when is_binary(token) do
Expand All @@ -37,7 +37,7 @@ defmodule Schlusseli.Plug.KeycloakCustomConnector do
"""
def verify_token(token, conn) do
with {:ok, claims} <- token_introspect(token),
true <- verify_audience(claims, get_provider_conf(:verify_token_audience)) do
true <- verify_audience(claims, get_provider_conf(:verify_token_audience)) do
conn
|> assign(:claims, claims)
else
Expand Down Expand Up @@ -88,9 +88,9 @@ defmodule Schlusseli.Plug.KeycloakCustomConnector do

defp auth_error(conn) do
conn
|> put_resp_content_type("application/vnd.api+json")
|> send_resp(401, Poison.encode!(%{error: :not_authorized}))
|> halt()
|> put_resp_content_type("application/vnd.api+json")
|> send_resp(401, Poison.encode!(%{error: :not_authorized}))
|> halt()
end

defp assert_json(%{"error" => reason}), do: {:error, reason}
Expand Down
24 changes: 14 additions & 10 deletions lib/schlusseli_web/plug/openid_connector.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
defmodule Schlusseli.Plug.OpenidConnector do
@moduledoc """
@moduledoc """
Plug for verifying authorization on a per request basis, verifies that a token is set in the
`Authorization` header.
Problem: OpenIDConnect verifies the token with the public key. No `introspec` is made. We don't know if the token is valid or not.
Problem: OpenIDConnect verifies the token with the public key. No `introspec` is made. We do not know if the token has been revoked or not.
"""

import Plug.Conn
Expand All @@ -19,9 +19,9 @@ defmodule Schlusseli.Plug.OpenidConnector do
@spec call(Plug.Conn.t(), keyword()) :: Plug.Conn.t()
def call(conn, _) do
conn
|> get_req_header("authorization")
|> fetch_token()
|> verify_token(conn)
|> get_req_header("authorization")
|> fetch_token()
|> verify_token(conn)
end

def fetch_token([token]) when is_binary(token) do
Expand All @@ -37,9 +37,9 @@ defmodule Schlusseli.Plug.OpenidConnector do
"""
def verify_token(token, conn, auth_provider \\ :keycloak) do
with {:ok, claims} <- OpenIDConnect.verify(auth_provider, token),
true <- verify_audience(claims, get_provider_conf(:verify_token_audience)) do
true <- verify_audience(claims, get_provider_conf(:verify_token_audience)) do
conn
|> assign(:claims, claims)
|> Absinthe.Plug.put_options(context: %{claims: normalize_claims(claims)})
else
_ -> auth_error(conn)
end
Expand All @@ -63,8 +63,12 @@ defmodule Schlusseli.Plug.OpenidConnector do

defp auth_error(conn) do
conn
|> put_resp_content_type("application/vnd.api+json")
|> send_resp(401, Poison.encode!(%{error: :not_authorized}))
|> halt()
|> put_resp_content_type("application/vnd.api+json")
|> send_resp(401, Poison.encode!(%{error: :not_authorized}))
|> halt()
end

defp normalize_claims(claims) do
for {key, val} <- claims, into: %{}, do: {String.to_atom(key), val}
end
end
4 changes: 1 addition & 3 deletions lib/schlusseli_web/resolver/customer.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
defmodule SchlusseliWeb.Resolvers.Customer do

alias Schlusseli.Factory

def list_customers(_parent, _args, _resolution) do
def list_customers(_parent, _args, %{context: %{claims: _context}}) do
{:ok, Factory.build_list(10, :customer)}
end

end
2 changes: 0 additions & 2 deletions lib/schlusseli_web/resolver/key.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
defmodule SchlusseliWeb.Resolvers.Key do

alias Schlusseli.Factory

def list_keys(_parent, _args, _resolution) do
{:ok, Factory.build_list(10, :key)}
end

end
2 changes: 1 addition & 1 deletion lib/schlusseli_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule SchlusseliWeb.Router do

pipeline :api do
plug :accepts, ["json"]
# plug(Schlusseli.Plug.OpenidConnector)
plug(Schlusseli.Plug.OpenidConnector)
end

scope "/", SchlusseliWeb do
Expand Down
12 changes: 5 additions & 7 deletions lib/schlusseli_web/schema.ex
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
defmodule SchlusseliWeb.Schema do
use Absinthe.Schema

import_types SchlusseliWeb.Schema.KeyTypes
import_types SchlusseliWeb.Schema.CustomerTypes
import_types(SchlusseliWeb.Schema.KeyTypes)
import_types(SchlusseliWeb.Schema.CustomerTypes)

alias SchlusseliWeb.Resolvers

query do

@desc "Get all keys"
field :keys, list_of(:key) do
resolve &Resolvers.Key.list_keys/3
resolve(&Resolvers.Key.list_keys/3)
end

@desc "Get all customers"
field :customers, list_of(:customer) do
resolve &Resolvers.Customer.list_customers/3
middleware(SchlusseliWeb.Schema.Middleware.Authorize, "view_customers")
resolve(&Resolvers.Customer.list_customers/3)
end

end

end
16 changes: 16 additions & 0 deletions lib/schlusseli_web/schema/middleware/authorize.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule SchlusseliWeb.Schema.Middleware.Authorize do
@behaviour Absinthe.Middleware

def call(resolution, scope) do
with %{claims: %{scope: current_scopes}} <- resolution.context,
true <- correct_scope?(current_scopes, scope) do
resolution
else
_ ->
resolution
|> Absinthe.Resolution.put_result({:error, "unauthorized"})
end
end

defp correct_scope?(current_scopes, scope), do: String.contains?(current_scopes, scope)
end
Loading

0 comments on commit fdfd1cc

Please sign in to comment.