Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Automated UI-tests using Cypress make sure that all examples work and that code changes do not introduce any unintended API changes.
[#227](https://github.com/Accenture/reactive-interaction-gateway/issues/227)
- Refactor JWT related code in favor of `RIG.JWT`.
[#244](https://github.com/Accenture/reactive-interaction-gateway/pull/244)

## [2.2.1] - 2019-06-21

Expand Down
19 changes: 19 additions & 0 deletions apps/rig/lib/rig/jwt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,25 @@ defmodule RIG.JWT do

# ---

@doc "Checks an encoded JWT for validity."
@callback valid?(token, jwt_conf, ensure_not_blacklisted) :: boolean()
def valid?(
token,
jwt_conf \\ config().jwt_conf,
ensure_not_blacklisted \\ &ensure_not_blacklisted/1
)

def valid?(token, jwt_conf, ensure_not_blacklisted) do
token
|> parse_token(jwt_conf, ensure_not_blacklisted)
|> case do
{:ok, _} -> true
_ -> false
end
end

# ---

defp ensure_not_blacklisted(%{"jti" => jti} = claims) do
if Blacklist.contains_jti?(Blacklist, jti) do
{:error, "Ignoring blacklisted JWT with ID #{jti}."}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,63 +1,46 @@
defmodule RigAuth.Jwt.UtilsTest do
defmodule RIG.JWTTest do
@moduledoc false
# Cannot be async because JWT related environment variables are modified:
use ExUnit.Case, async: false
use ExUnit.Case, async: true
use RigAuth.ConnCase

alias RigAuth.Jwt.Utils

setup do
env_jwt_key = Confex.fetch_env!(:rig, RigAuth.Jwt.Utils)[:secret_key]
env_jwt_alg = Confex.fetch_env!(:rig, RigAuth.Jwt.Utils)[:alg]

on_exit(fn ->
System.put_env("JWT_SECRET_KEY", env_jwt_key)
System.put_env("JWT_ALG", env_jwt_alg)
end)

System.put_env("JWT_SECRET_KEY", "mysecret")
System.put_env("JWT_ALG", "HS256")
end
alias RIG.JWT

describe "decode/1" do
test "should return decoded jwt payload with valid jwt" do
jwt = generate_jwt()
assert {:ok, _decoded_payload} = Utils.decode(jwt)
conf = %{alg: "HS256", key: "mysecret"}
jwt = JWT.encode(%{"foo" => "bar"}, conf)
assert {:ok, %{"foo" => "bar"}} = JWT.parse_token(jwt, conf)
end

test "should return error with invalid jwt" do
jwt = "badtoken"
assert {:error, "Invalid signature"} = Utils.decode(jwt)
illegal_jwt = "badtoken"
assert {:error, %JWT.DecodeError{}} = JWT.parse_token(illegal_jwt)
end
end

describe "valid?/1" do
test "should return true with valid jwt using RS256" do
System.put_env(
"JWT_SECRET_KEY",
pubkey =
"-----BEGIN CERTIFICATE-----\nMIICVzCCAcACCQC6Bxn5zZYgBzANBgkqhkiG9w0BAQsFADBwMQswCQYDVQQGEwJx\ndzEMMAoGA1UECAwDcXdlMQwwCgYDVQQHDANxd2UxDDAKBgNVBAoMA3F3ZTEMMAoG\nA1UECwwDcXdlMQwwCgYDVQQDDANxd2UxGzAZBgkqhkiG9w0BCQEWDHF3ZUBtYWls\nLmNvbTAeFw0xODA3MjMxMjA1MTFaFw0yMzA3MjIxMjA1MTFaMHAxCzAJBgNVBAYT\nAnF3MQwwCgYDVQQIDANxd2UxDDAKBgNVBAcMA3F3ZTEMMAoGA1UECgwDcXdlMQww\nCgYDVQQLDANxd2UxDDAKBgNVBAMMA3F3ZTEbMBkGCSqGSIb3DQEJARYMcXdlQG1h\naWwuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQChEGtKvkROJocIBNxT\n0inEGEdQPxqvbFol2cMNiQikg7DCx91qc2FQ2hfZSuJiVcQSfVfJvyVO66pM7+bZ\nV/845LhDaUUhzp18pv0PGrIIxNMVD+A25vwq9ay6qlOZz1LNW1OSWpUlqK0Cw/LD\nlO6qFQLVNfudC0hRpVKLYC3hvQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAJNtoxWp\n0gC1437hF09a8MkCxh2JrtZTJLUozYedwtmkBFhHi1vvpMgFRkPPaGnSI14H4nyJ\nSBjUBRWLkf0+NKMP8OR8VW2qQ2F/o0fkcW+3CHBt+8b3basDxVb2ooFq599P1qB4\n3R0T687G1c8pQ98CBN5gaBvtNldenM0QxXhn\n-----END CERTIFICATE-----"
)

System.put_env("JWT_ALG", "RS256")

jwt =
privkey =
"-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQChEGtKvkROJocIBNxT0inEGEdQPxqvbFol2cMNiQikg7DCx91q\nc2FQ2hfZSuJiVcQSfVfJvyVO66pM7+bZV/845LhDaUUhzp18pv0PGrIIxNMVD+A2\n5vwq9ay6qlOZz1LNW1OSWpUlqK0Cw/LDlO6qFQLVNfudC0hRpVKLYC3hvQIDAQAB\nAoGAKAWpc4g19ulx8lcq3JVDlZum1NTpb5/QAsnKwylDAYZLvQrnBRWon+uhs3f9\nKww+zY1h7BrYTXUX+0g9p9JK89Ysto2HncjEC9vMm8Gb0feFcBDOJlYrom1SA47N\n+MqJeg1LiDfHIVBXs4W7h5u1kFZhN6MNtYyzOsKkilL7PmECQQDV3Tpi/i3kyMSw\nGCYdsDUHE/sMGIW9VaetgYdvHDd+tPO3mwMe0lrItUyNtzDrJR4K/MmI6sWP4raN\nd4mfnf0lAkEAwMwW/urkQBLNgNRpPojiWvy2+ZsXmtW15qaWlaEffzjfFQ2IoEzs\nLZe6Rsj4BZ7p53JXILJS3JzevQFFEpyKuQJAVwFHjZpmxVrAWfuZFh7nk9eXHJal\nYh+EtduqY5ORKCUpuZqArHtbn6fSWx0Z87AIBuRMgT0x3pWXOvpUrPEzWQJAccuE\nfy3xTwhKF5JIFEsDH6Ut8qHiCte9J8iH9QVG6/aLZYe5brQ4aqi1n/YavmaPtLY+\nSuQ2GFTW+0P2mweesQJBAKic+GSd/eNTBnJUJsTJMuZjGaHyfvqd0LWVGnQx7xPM\n4I6jG6djGZI1+ImIxIkd+KPuTeEYjFzTEN+rzcoUtLw=\n-----END RSA PRIVATE KEY-----"
|> generate_jwt

assert Utils.valid?("Bearer " <> jwt)
# Encode with the private key:
jwt = JWT.encode(%{}, %{alg: "RS256", key: privkey})

# Verify with the public key:
assert JWT.valid?(jwt, %{alg: "RS256", key: pubkey})
end

test "should return true with valid jwt using HS256" do
jwt = generate_jwt()
assert Utils.valid?("Bearer " <> jwt)
conf = %{alg: "HS256", key: "mysecret"}
assert %{} |> JWT.encode(conf) |> JWT.valid?(conf)
end

test "should return false with invalid jwt" do
jwt = "badtoken"

assert Utils.valid?(jwt) == %{
error: "JWT=badtoken is missing token type. Required format is: \"Bearer token\""
}
conf = %{alg: "HS256", key: "mysecret"}
refute "badtoken" |> JWT.valid?(conf)
end
end
end
12 changes: 1 addition & 11 deletions apps/rig/test/rig/subscriptions_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ defmodule RIG.SubscriptionsTest do
@moduledoc false
use ExUnit.Case, async: false

import Joken

alias RIG.JWT
alias RIG.Subscriptions
alias RigInboundGateway.ExtractorConfig
Expand Down Expand Up @@ -79,13 +77,5 @@ defmodule RIG.SubscriptionsTest do
Subscriptions.from_token("Bearer #{jwt}", @jwt_conf)
end

defp generate_jwt do
token()
|> with_exp
|> with_signer(@jwt_secret_key |> hs256)
|> with_claim("username", "john")
|> with_claim("fullname", "John Doe")
|> sign
|> get_compact
end
defp generate_jwt, do: JWT.encode(%{"username" => "john", "fullname" => "John Doe"}, @jwt_conf)
end
5 changes: 3 additions & 2 deletions apps/rig_api/lib/rig_api/controllers/channels_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ defmodule RigApi.ChannelsController do
"""
use RigApi, :controller
require Logger

alias RIG.JWT
alias RigAuth.Blacklist
alias RigAuth.Jwt
alias RigInboundGatewayWeb.Endpoint

def list_channels(conn, _params) do
Expand Down Expand Up @@ -44,7 +45,7 @@ defmodule RigApi.ChannelsController do
defp jwt_expiry_from_tokens([]), do: nil

defp jwt_expiry_from_tokens([token]) do
{:ok, %{"exp" => expiry}} = Jwt.Utils.decode(token)
{:ok, %{"exp" => expiry}} = JWT.parse_token(token)

expiry
# Comes as int, convert to string
Expand Down
5 changes: 3 additions & 2 deletions apps/rig_auth/lib/rig_auth/authorization_check/header.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ defmodule RigAuth.AuthorizationCheck.Header do
@moduledoc false

alias Plug

alias RIG.JWT
alias RigAuth.AuthorizationCheck.Request
alias RigAuth.Jwt.Utils, as: Jwt

# ---

@spec any_valid_bearer_token?(Request.t()) :: boolean
def any_valid_bearer_token?(request)

def any_valid_bearer_token?(%{auth_info: %{auth_tokens: tokens}}) do
for({"bearer", token} <- tokens, do: Jwt.valid?(token))
for({"bearer", token} <- tokens, do: JWT.valid?(token))
|> Enum.any?()
end

Expand Down
83 changes: 0 additions & 83 deletions apps/rig_auth/lib/rig_auth/jwt/utils.ex

This file was deleted.

20 changes: 0 additions & 20 deletions apps/rig_auth/test/support/conn_case.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,6 @@ defmodule RigAuth.ConnCase do
quote do
# Import conveniences for testing with connections
use Phoenix.ConnTest

import Joken

# Generation of JWT
def generate_jwt(priv_key \\ nil) do
jwt_secret_key = System.get_env("JWT_SECRET_KEY")
jwt_alg = System.get_env("JWT_ALG")

signer =
case jwt_alg do
"HS" <> _ = alg -> Joken.Signer.hs(alg, jwt_secret_key)
"RS" <> _ = alg -> Joken.Signer.rs(alg, JOSE.JWK.from_pem(priv_key))
end

token()
|> with_exp
|> with_signer(signer)
|> sign
|> get_compact
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,59 @@ defmodule RigInboundGateway.ApiProxy.Auth.Jwt do

import Plug.Conn, only: [get_req_header: 2]

alias RigAuth.Jwt
alias RIG.JWT
alias RigInboundGateway.ApiProxy.Api

# ---

def check(conn, api) do
tokens =
pick_query_token(conn, api)
|> Enum.concat(pick_header_token(conn, api))
|> Enum.reject(&(&1 == ""))

if Enum.any?(tokens, &Jwt.Utils.valid?/1) do
:ok
else
{:error, :authentication_failed}
valid_tokens =
tokens_from_query_params(conn, api) ++
tokens_from_req_header(conn, api)

case valid_tokens do
[] -> {:error, :authentication_failed}
_ -> :ok
end
end

# ---

# Pick JWT from query parameters
@spec pick_query_token(Plug.Conn.t(), Api.t()) :: [String.t()]
# Find and parse JWTs in request query params.
@spec tokens_from_query_params(Plug.Conn.t(), Api.t()) :: [JWT.claims()]

defp tokens_from_query_params(conn, %{"auth" => %{"use_query" => true}} = api) do
selected_param = get_in(api, ["auth", "query_name"])
tokens_string = Map.get(conn.query_params, selected_param, "")

defp pick_query_token(conn, %{"auth" => %{"use_query" => true}} = api) do
conn
|> Map.get(:query_params)
|> Map.get(Kernel.get_in(api, ["auth", "query_name"]), "")
|> String.split()
tokens_string
|> String.split(",", trim: true)
|> Enum.map(&JWT.parse_token/1)
|> Result.filter_and_unwrap()
end

defp pick_query_token(_conn, %{"auth" => %{"use_query" => false}}), do: [""]
defp tokens_from_query_params(_conn, _), do: []

# --

# Pick JWT from headers
@spec pick_header_token(Plug.Conn.t(), Api.t()) :: [String.t()]
# Find and parse JWTs in request headers.
@spec tokens_from_req_header(Plug.Conn.t(), Api.t()) :: [JWT.claims()]

defp tokens_from_req_header(conn, %{"auth" => %{"use_header" => true}} = api) do
selected_auth_header = get_in(api, ["auth", "header_name"]) |> String.downcase()

defp pick_header_token(conn, %{"auth" => %{"use_header" => true}} = api) do
header_key = Kernel.get_in(api, ["auth", "header_name"]) |> String.downcase()
get_req_header(conn, header_key)
case selected_auth_header do
"authorization" ->
JWT.parse_http_header(conn.req_headers)
|> Result.filter_and_unwrap()

"x-" <> _ = custom_header ->
conn
|> get_req_header(custom_header)
|> Enum.map(&JWT.parse_token/1)
|> Result.filter_and_unwrap()
end
end

defp pick_header_token(_conn, %{"auth" => %{"use_header" => false}}), do: [""]
defp tokens_from_req_header(_conn, _), do: []
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule RigInboundGateway.ApiProxy.ProxyMetricsTest do
import FakeServer
alias FakeServer.Response

alias RIG.JWT
alias RigMetrics.ProxyMetrics

alias RigInboundGatewayWeb.Router
Expand Down Expand Up @@ -93,7 +94,7 @@ defmodule RigInboundGateway.ApiProxy.ProxyMetricsTest do
# ---

defp construct_request_with_jwt(method, url, query \\ %{}) do
jwt = generate_jwt()
jwt = JWT.encode(%{})

build_conn(method, url, query)
|> put_req_header("authorization", "Bearer #{jwt}")
Expand Down
Loading