1defmodule JwtAuthTest do
2  use CouchTestCase
3
4  @moduletag :authentication
5
6  test "jwt auth with HMAC secret", _context do
7
8    secret = "zxczxc12zxczxc12"
9
10    server_config = [
11      %{
12        :section => "jwt_keys",
13        :key => "hmac:_default",
14        :value => :base64.encode(secret)
15      },
16      %{
17        :section => "jwt_auth",
18        :key => "allowed_algorithms",
19        :value => "HS256, HS384, HS512"
20      }
21    ]
22
23    run_on_modified_server(server_config, fn -> test_fun("HS256", secret) end)
24    run_on_modified_server(server_config, fn -> test_fun("HS384", secret) end)
25    run_on_modified_server(server_config, fn -> test_fun("HS512", secret) end)
26  end
27
28  defmodule RSA do
29    require Record
30    Record.defrecord :public, :RSAPublicKey,
31      Record.extract(:RSAPublicKey, from_lib: "public_key/include/public_key.hrl")
32    Record.defrecord :private, :RSAPrivateKey,
33      Record.extract(:RSAPrivateKey, from_lib: "public_key/include/public_key.hrl")
34  end
35
36  test "jwt auth with RSA secret", _context do
37    require JwtAuthTest.RSA
38
39    private_key = :public_key.generate_key({:rsa, 2048, 17})
40    public_key = RSA.public(
41      modulus: RSA.private(private_key, :modulus),
42      publicExponent: RSA.private(private_key, :publicExponent))
43
44    public_pem = :public_key.pem_encode(
45      [:public_key.pem_entry_encode(
46          :SubjectPublicKeyInfo, public_key)])
47    public_pem = String.replace(public_pem, "\n", "\\n")
48
49    server_config = [
50      %{
51        :section => "jwt_keys",
52        :key => "rsa:_default",
53        :value => public_pem
54      },
55      %{
56        :section => "jwt_auth",
57        :key => "allowed_algorithms",
58        :value => "RS256, RS384, RS512"
59      }
60    ]
61
62    run_on_modified_server(server_config, fn -> test_fun("RS256", private_key) end)
63    run_on_modified_server(server_config, fn -> test_fun("RS384", private_key) end)
64    run_on_modified_server(server_config, fn -> test_fun("RS512", private_key) end)
65  end
66
67  defmodule EC do
68    require Record
69    Record.defrecord :point, :ECPoint,
70      Record.extract(:ECPoint, from_lib: "public_key/include/public_key.hrl")
71    Record.defrecord :private, :ECPrivateKey,
72      Record.extract(:ECPrivateKey, from_lib: "public_key/include/public_key.hrl")
73  end
74
75  test "jwt auth with EC secret", _context do
76    require JwtAuthTest.EC
77
78    private_key = :public_key.generate_key({:namedCurve, :secp384r1})
79    point = EC.point(point: EC.private(private_key, :publicKey))
80    public_key = {point, EC.private(private_key, :parameters)}
81
82    public_pem = :public_key.pem_encode(
83      [:public_key.pem_entry_encode(
84          :SubjectPublicKeyInfo, public_key)])
85    public_pem = String.replace(public_pem, "\n", "\\n")
86
87    server_config = [
88      %{
89        :section => "jwt_keys",
90        :key => "ec:_default",
91        :value => public_pem
92      },
93      %{
94        :section => "jwt_auth",
95        :key => "allowed_algorithms",
96        :value => "ES256, ES384, ES512"
97      }
98    ]
99
100    run_on_modified_server(server_config, fn -> test_fun("ES256", private_key) end)
101    run_on_modified_server(server_config, fn -> test_fun("ES384", private_key) end)
102    run_on_modified_server(server_config, fn -> test_fun("ES512", private_key) end)
103  end
104
105  def test_fun(alg, key) do
106    now = DateTime.to_unix(DateTime.utc_now())
107    {:ok, token} = :jwtf.encode(
108      {
109        [
110          {"alg", alg},
111          {"typ", "JWT"}
112        ]
113      },
114      {
115        [
116          {"nbf", now - 60},
117          {"exp", now + 60},
118          {"sub", "couch@apache.org"},
119          {"_couchdb.roles", ["testing"]
120          }
121        ]
122      }, key)
123
124    resp = Couch.get("/_session",
125      headers: [authorization: "Bearer #{token}"]
126    )
127
128    assert resp.body["userCtx"]["name"] == "couch@apache.org"
129    assert resp.body["info"]["authenticated"] == "jwt"
130  end
131
132  test "jwt auth without secret", _context do
133
134    resp = Couch.get("/_session")
135
136    assert resp.body["userCtx"]["name"] == "adm"
137    assert resp.body["info"]["authenticated"] == "default"
138  end
139
140  test "jwt auth with required iss claim", _context do
141
142    secret = "zxczxc12zxczxc12"
143
144    server_config = [
145      %{
146        :section => "jwt_auth",
147        :key => "required_claims",
148        :value => "{iss, \"hello\"}"
149      },
150      %{
151        :section => "jwt_keys",
152        :key => "hmac:_default",
153        :value => :base64.encode(secret)
154      },
155      %{
156        :section => "jwt_auth",
157        :key => "allowed_algorithms",
158        :value => "HS256, HS384, HS512"
159      }
160    ]
161
162    run_on_modified_server(server_config, fn -> good_iss("HS256", secret) end)
163    run_on_modified_server(server_config, fn -> bad_iss("HS256", secret) end)
164  end
165
166  def good_iss(alg, key) do
167    {:ok, token} = :jwtf.encode(
168      {
169        [
170          {"alg", alg},
171          {"typ", "JWT"}
172        ]
173      },
174      {
175        [
176          {"iss", "hello"},
177          {"sub", "couch@apache.org"},
178          {"_couchdb.roles", ["testing"]
179          }
180        ]
181      }, key)
182
183    resp = Couch.get("/_session",
184      headers: [authorization: "Bearer #{token}"]
185    )
186
187    assert resp.body["userCtx"]["name"] == "couch@apache.org"
188    assert resp.body["userCtx"]["roles"] == ["testing"]
189    assert resp.body["info"]["authenticated"] == "jwt"
190  end
191
192  def bad_iss(alg, key) do
193    {:ok, token} = :jwtf.encode(
194      {
195        [
196          {"alg", alg},
197          {"typ", "JWT"}
198        ]
199      },
200      {
201        [
202          {"iss", "goodbye"},
203          {"sub", "couch@apache.org"},
204          {"_couchdb.roles", ["testing"]
205          }
206        ]
207      }, key)
208
209    resp = Couch.get("/_session",
210      headers: [authorization: "Bearer #{token}"]
211    )
212
213    assert resp.status_code == 400
214  end
215
216end
217