Compare commits
1454
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bc7857637 | ||
|
|
5b70bbe790 | ||
|
|
77bf345519 | ||
|
|
fdb80d4319 | ||
|
|
f0f1b85582 | ||
|
|
6874cdc3b2 | ||
|
|
19bdf09e3d | ||
|
|
999dff353c | ||
|
|
f5389b0e17 | ||
|
|
b4ad69b841 | ||
|
|
6ba3687b76 | ||
|
|
500a21685f | ||
|
|
620bea3553 | ||
|
|
5e225c2bd5 | ||
|
|
c9bc321d2a | ||
|
|
74fe9249d5 | ||
|
|
9f814c6cf1 | ||
|
|
89fcddd605 | ||
|
|
0b108d83af | ||
|
|
5e16a32f86 | ||
|
|
6b846b53e4 | ||
|
|
e964b156cc | ||
|
|
bdc9e8d2c3 | ||
|
|
ce4506f367 | ||
|
|
55d0ad3a50 | ||
|
|
d90235d1bd | ||
|
|
1220d7f898 | ||
|
|
f5545eabdc | ||
|
|
3db8f97248 | ||
|
|
48421cbc43 | ||
|
|
7010c39544 | ||
|
|
a3ccac2012 | ||
|
|
6bbdc13544 | ||
|
|
073f1eda54 | ||
|
|
29c2ef4c60 | ||
|
|
947254e14f | ||
|
|
b02d48f772 | ||
|
|
4be0e36306 | ||
|
|
b041146a3f | ||
|
|
56c5996aff | ||
|
|
fa165806f9 | ||
|
|
7849191ec7 | ||
|
|
06a318d7d5 | ||
|
|
361ab1c988 | ||
|
|
c1c1a81a4e | ||
|
|
b4d39ca061 | ||
|
|
b4470165c7 | ||
|
|
af63fcbc4d | ||
|
|
d77824ff98 | ||
|
|
b771960e21 | ||
|
|
bc05560d4d | ||
|
|
3855b5c0a3 | ||
|
|
28b3ef9aa9 | ||
|
|
22987bb90b | ||
|
|
1c10199698 | ||
|
|
4367f03304 | ||
|
|
955cbc76d7 | ||
|
|
f1807aead4 | ||
|
|
756d996a4a | ||
|
|
3ee86fedc7 | ||
|
|
e7f781df5a | ||
|
|
8f9ed32a66 | ||
|
|
39dea4b078 | ||
|
|
58e5f41415 | ||
|
|
a7fbcaaef4 | ||
|
|
e75f47d523 | ||
|
|
cc14c5db23 | ||
|
|
6f731dfee9 | ||
|
|
ab7a0c2252 | ||
|
|
d08ddade2e | ||
|
|
52e40922bc | ||
|
|
2b1613d36b | ||
|
|
f1132ffabe | ||
|
|
8c82201add | ||
|
|
fc61a4e89f | ||
|
|
f289771fd3 | ||
|
|
8676148f20 | ||
|
|
bf43ccf6c1 | ||
|
|
eebd3feb28 | ||
|
|
be5291f710 | ||
|
|
3fd6fc250d | ||
|
|
e91dd011c5 | ||
|
|
8e36a52f1e | ||
|
|
9edb5b53c0 | ||
|
|
eae0360eb1 | ||
|
|
c7a20d7620 | ||
|
|
93d57c7d00 | ||
|
|
32cf8b7cad | ||
|
|
88d9411f4d | ||
|
|
2e2d15be9f | ||
|
|
f363dbcad0 | ||
|
|
63d2d0f7ed | ||
|
|
a04d79c576 | ||
|
|
57cb46148f | ||
|
|
7614ff8c6f | ||
|
|
edc6d99c9c | ||
|
|
a994af9010 | ||
|
|
0b5e6b0d49 | ||
|
|
222541ab5c | ||
|
|
dfed148921 | ||
|
|
a785f9fd11 | ||
|
|
93105a2182 | ||
|
|
0ac92c4bfa | ||
|
|
98a5c57e65 | ||
|
|
5d72def1bc | ||
|
|
f0d1149311 | ||
|
|
d4a85d6abc | ||
|
|
a34f890453 | ||
|
|
f8999128dc | ||
|
|
91585f8563 | ||
|
|
bdadef1282 | ||
|
|
5028c05cc2 | ||
|
|
6202222da4 | ||
|
|
60942346f3 | ||
|
|
97509ebf89 | ||
|
|
169280b6a1 | ||
|
|
35857bf649 | ||
|
|
bc407a7968 | ||
|
|
473065fd31 | ||
|
|
a0f3a6469f | ||
|
|
fd8a3dccaf | ||
|
|
f12cef2c86 | ||
|
|
2b55cd7902 | ||
|
|
a9b9d16500 | ||
|
|
80f8815b9a | ||
|
|
6cccd66162 | ||
|
|
84d2d7b431 | ||
|
|
9135ba7c40 | ||
|
|
94528976d4 | ||
|
|
7381ec456a | ||
|
|
536a57a514 | ||
|
|
5d88c2b8be | ||
|
|
9be9e30ad6 | ||
|
|
40c4140021 | ||
|
|
23887f4031 | ||
|
|
3760f2753b | ||
|
|
d0f505455d | ||
|
|
a41d282e92 | ||
|
|
813af5d757 | ||
|
|
241c58dd61 | ||
|
|
1cddaeb707 | ||
|
|
d69d00082f | ||
|
|
b9f7a43a72 | ||
|
|
bed6f96fc6 | ||
|
|
4d5e03049e | ||
|
|
7d017b4edf | ||
|
|
dcd1a68d95 | ||
|
|
b71bb96540 | ||
|
|
c4a0470a37 | ||
|
|
a52a1f4618 | ||
|
|
a2e35bf711 | ||
|
|
a6e4245185 | ||
|
|
67bee0dfc2 | ||
|
|
8f1938c28d | ||
|
|
a15517c5ce | ||
|
|
af6f617e0e | ||
|
|
8c53c5a90e | ||
|
|
ca4488a845 | ||
|
|
7d0b461683 | ||
|
|
c3ea847b3e | ||
|
|
684689d40d | ||
|
|
c47952250d | ||
|
|
c4f172afe7 | ||
|
|
c43329d033 | ||
|
|
5560175888 | ||
|
|
bb1096c511 | ||
|
|
88473ce3c1 | ||
|
|
81f1a5b145 | ||
|
|
e4fc8f083e | ||
|
|
79af4dfd18 | ||
|
|
9e509d71d2 | ||
|
|
2b59db71a8 | ||
|
|
63c237423d | ||
|
|
b870e81648 | ||
|
|
03308d8b20 | ||
|
|
73c328d5e3 | ||
|
|
829bcf5eaf | ||
|
|
554e692d8f | ||
|
|
366cb78a36 | ||
|
|
1d5bd2567e | ||
|
|
67a05ee227 | ||
|
|
a9c98a577b | ||
|
|
c442bcd8d3 | ||
|
|
c00a64208a | ||
|
|
e59aeb56cc | ||
|
|
e3a04ebd90 | ||
|
|
06152d9883 | ||
|
|
c230edbf72 | ||
|
|
1b6405a28f | ||
|
|
12fec09410 | ||
|
|
ef29097679 | ||
|
|
0a869f56a7 | ||
|
|
cd53486566 | ||
|
|
ff2d6941d0 | ||
|
|
acd78ee407 | ||
|
|
130452f6c3 | ||
|
|
178fd08d9a | ||
|
|
097a2267f5 | ||
|
|
7afdd98499 | ||
|
|
f47283f61a | ||
|
|
03b1dff1bd | ||
|
|
804766827c | ||
|
|
8adde64e2c | ||
|
|
ca8577fb4a | ||
|
|
37a98fba20 | ||
|
|
ef6b303bfe | ||
|
|
8d40ee17f1 | ||
|
|
29c5d683d0 | ||
|
|
3580dea399 | ||
|
|
7ae117707b | ||
|
|
d1d590b45c | ||
|
|
62ffab7239 | ||
|
|
7681c76ee1 | ||
|
|
ab763e7c5b | ||
|
|
c2379dbe49 | ||
|
|
f114895cd2 | ||
|
|
ce9320755d | ||
|
|
dce4353e0d | ||
|
|
4d5ca2b8fa | ||
|
|
8e4899f874 | ||
|
|
e2a7b71cf3 | ||
|
|
f0386d9374 | ||
|
|
336d05dd34 | ||
|
|
d80381ec0d | ||
|
|
d4f870fbb4 | ||
|
|
0b6c9aa927 | ||
|
|
ca8d756c05 | ||
|
|
a2af516df7 | ||
|
|
43a625db28 | ||
|
|
cc42ea9332 | ||
|
|
9ec3a8c3d9 | ||
|
|
2e83eda172 | ||
|
|
6d8a3f7a8b | ||
|
|
56d68c17f5 | ||
|
|
dac848e164 | ||
|
|
badabf8385 | ||
|
|
d22a284df1 | ||
|
|
60ef910b5b | ||
|
|
53ab314eb9 | ||
|
|
2cac8b1bb7 | ||
|
|
c291748f43 | ||
|
|
c6ff1037dc | ||
|
|
b68494b3f0 | ||
|
|
39afd4a020 | ||
|
|
355af6d1cf | ||
|
|
69ada965d8 | ||
|
|
f9767ad6cd | ||
|
|
28acf489a1 | ||
|
|
86f95d3f87 | ||
|
|
c178a2392d | ||
|
|
10c9e0f00f | ||
|
|
71f0db0837 | ||
|
|
27b291c345 | ||
|
|
4581272dcd | ||
|
|
70d7e07526 | ||
|
|
7d0ca42c6c | ||
|
|
9804aaf796 | ||
|
|
954ef99f22 | ||
|
|
789722ec6a | ||
|
|
d824b4d93c | ||
|
|
7569493b17 | ||
|
|
aaddb035a8 | ||
|
|
cc6a5419de | ||
|
|
7d8796e614 | ||
|
|
b8262a9f02 | ||
|
|
db8b733610 | ||
|
|
c431cc1b01 | ||
|
|
168153c6e7 | ||
|
|
47aaa5a636 | ||
|
|
e7272dc05a | ||
|
|
b55b089355 | ||
|
|
a9ec69e424 | ||
|
|
19a41c8704 | ||
|
|
83b29c5080 | ||
|
|
f392e79ad7 | ||
|
|
e4aba9e471 | ||
|
|
c19d6f36ed | ||
|
|
14908d9038 | ||
|
|
c3a2b3a6b6 | ||
|
|
9378ede362 | ||
|
|
349a59cf35 | ||
|
|
64cd689e38 | ||
|
|
bfb4395b28 | ||
|
|
46ec12c41c | ||
|
|
0d9ffd4e64 | ||
|
|
d39d39a8eb | ||
|
|
b64817761a | ||
|
|
f3febd1067 | ||
|
|
ec53af29c5 | ||
|
|
c0b08a27a2 | ||
|
|
18295497ae | ||
|
|
978619ead0 | ||
|
|
12e56086d5 | ||
|
|
a8a8ea4bcd | ||
|
|
f760427c5c | ||
|
|
126830cd8b | ||
|
|
e738b84fd5 | ||
|
|
59d5604699 | ||
|
|
68c7da141b | ||
|
|
f63c60855e | ||
|
|
f0a0b553e5 | ||
|
|
e14be4460b | ||
|
|
2d825dd366 | ||
|
|
f4f9a98e70 | ||
|
|
47a8077426 | ||
|
|
e1b6ba13cc | ||
|
|
8da766e02e | ||
|
|
a8071356bb | ||
|
|
ec5860d7f5 | ||
|
|
7c2fcaf141 | ||
|
|
e0da7ed6b4 | ||
|
|
5e1654658f | ||
|
|
e6477ed261 | ||
|
|
43f64e51c6 | ||
|
|
b00651391b | ||
|
|
2e66cd1070 | ||
|
|
55f3d8c1d0 | ||
|
|
d5c643eaf9 | ||
|
|
ee327b7eec | ||
|
|
5fd704af58 | ||
|
|
aa1eb334e0 | ||
|
|
47ea1ade8a | ||
|
|
b33f7414da | ||
|
|
16aaa3bf01 | ||
|
|
b8fecca0ba | ||
|
|
b824c4195a | ||
|
|
1438982104 | ||
|
|
c3b44e61d4 | ||
|
|
93ef2ec9bf | ||
|
|
af792b8abe | ||
|
|
1f4311a02c | ||
|
|
ff6dbd6bde | ||
|
|
2df1175315 | ||
|
|
eb93eef874 | ||
|
|
5b926a3da6 | ||
|
|
618a8e7f90 | ||
|
|
5a8634e359 | ||
|
|
bbed1ffca7 | ||
|
|
09ecc37ccb | ||
|
|
ab8658333f | ||
|
|
ca1152c3e5 | ||
|
|
7bcbc9a6af | ||
|
|
6073f4c159 | ||
|
|
dee4548798 | ||
|
|
8f9b6a5e44 | ||
|
|
87dedb56e5 | ||
|
|
6e9b9bedbb | ||
|
|
57bf9254e1 | ||
|
|
f0ac725984 | ||
|
|
c0e03c05c7 | ||
|
|
14cf2c9aba | ||
|
|
0781f3b33d | ||
|
|
b683e43ea7 | ||
|
|
51e4beccf4 | ||
|
|
c7bdb489ee | ||
|
|
2b84b8255e | ||
|
|
0cad34abbe | ||
|
|
389e2b42f1 | ||
|
|
2276f99d54 | ||
|
|
f67013bcb6 | ||
|
|
4e43518b2a | ||
|
|
cc27dd485b | ||
|
|
b78cc63f0d | ||
|
|
b84507507d | ||
|
|
fbcfcfa088 | ||
|
|
0809033924 | ||
|
|
d6a0c9e186 | ||
|
|
b37ed7145c | ||
|
|
7c2ea4db98 | ||
|
|
4b417c5ae8 | ||
|
|
2913ee8e23 | ||
|
|
eedb40bbe0 | ||
|
|
f68b28db20 | ||
|
|
c83e192a6f | ||
|
|
d7615d6a68 | ||
|
|
9c0e7cb4a5 | ||
|
|
f1f2040910 | ||
|
|
1f98dc5527 | ||
|
|
754b0fd123 | ||
|
|
0ccb98a81f | ||
|
|
5b5cfb01b9 | ||
|
|
b3131bf679 | ||
|
|
79c9078caa | ||
|
|
4b01c8e4dc | ||
|
|
a3c57f4326 | ||
|
|
ab58779d22 | ||
|
|
38ae6c981b | ||
|
|
e678a0362b | ||
|
|
46bc102799 | ||
|
|
8000baeb1f | ||
|
|
cf7f28d97e | ||
|
|
84f5df4814 | ||
|
|
d081f967ea | ||
|
|
54ee055bd8 | ||
|
|
e48340121b | ||
|
|
b81891f89b | ||
|
|
1385d3c219 | ||
|
|
49547ccfbc | ||
|
|
e8a65017fa | ||
|
|
5e57e84384 | ||
|
|
5972c94dc8 | ||
|
|
cb7cef8858 | ||
|
|
c40d79afb0 | ||
|
|
12cb295a35 | ||
|
|
58868ca99f | ||
|
|
d05a894831 | ||
|
|
dfec691de0 | ||
|
|
03e6c0884a | ||
|
|
e0f753bcf5 | ||
|
|
b84f6d306d | ||
|
|
1fd5af840b | ||
|
|
8dc65cff0a | ||
|
|
3dbd0d5063 | ||
|
|
6c09df0cae | ||
|
|
09bc16e6c5 | ||
|
|
f92ad70ff0 | ||
|
|
15f1b948bf | ||
|
|
95fc8f79dd | ||
|
|
f50e08d111 | ||
|
|
257a973995 | ||
|
|
606c385f26 | ||
|
|
e6486ab1c1 | ||
|
|
b45089f5f0 | ||
|
|
e85d8774da | ||
|
|
62000e042d | ||
|
|
fa1b998802 | ||
|
|
df163d86e8 | ||
|
|
70a3dd1e4a | ||
|
|
80a80512ea | ||
|
|
4f66919474 | ||
|
|
bf69bc94bb | ||
|
|
585653b88e | ||
|
|
4d5fd0b75e | ||
|
|
2a8b2aabb9 | ||
|
|
1d03eb5f2b | ||
|
|
4da07474fa | ||
|
|
14b109da18 | ||
|
|
7de0b329a2 | ||
|
|
81123e0eea | ||
|
|
8919015f74 | ||
|
|
b72166ebf1 | ||
|
|
10009868a3 | ||
|
|
0a0d2174e4 | ||
|
|
038c4765a3 | ||
|
|
c7a97cfd21 | ||
|
|
19a59429ae | ||
|
|
0b21ed3e0e | ||
|
|
906739f8f3 | ||
|
|
c9630157e8 | ||
|
|
51eef9b8b3 | ||
|
|
330663dfd3 | ||
|
|
e54c1d7de1 | ||
|
|
da6781dd91 | ||
|
|
4f771ac51a | ||
|
|
5ccf412fe2 | ||
|
|
ac6eb79b53 | ||
|
|
a415ea3670 | ||
|
|
32b62b2a70 | ||
|
|
4344ceefc2 | ||
|
|
fa97c2419e | ||
|
|
964693077a | ||
|
|
2a1a19f290 | ||
|
|
2336e97bf0 | ||
|
|
b6978a424f | ||
|
|
8e877a7f6c | ||
|
|
886528dab8 | ||
|
|
9a2d36aa9d | ||
|
|
ea9c663ae1 | ||
|
|
eec712a259 | ||
|
|
54ebc1918c | ||
|
|
d60dcb4c62 | ||
|
|
8e64bc3e4d | ||
|
|
cf1e8d1dd8 | ||
|
|
0b8fc3d6e1 | ||
|
|
55f747ecd0 | ||
|
|
a011f44d39 | ||
|
|
00b832b1cd | ||
|
|
8840bc073f | ||
|
|
bfd76d7813 | ||
|
|
2066259518 | ||
|
|
9bca7fb7c5 | ||
|
|
cf095766c7 | ||
|
|
c73de4f2b9 | ||
|
|
4daafa56fc | ||
|
|
16430791bb | ||
|
|
5e2446f838 | ||
|
|
b18c8c92c5 | ||
|
|
e8bbfd9012 | ||
|
|
94d375886f | ||
|
|
f9c017efaf | ||
|
|
0db2640076 | ||
|
|
ebabc6fd8a | ||
|
|
56320cc023 | ||
|
|
e9d22f3506 | ||
|
|
83d0f313ce | ||
|
|
cac19c8a8f | ||
|
|
9452d37926 | ||
|
|
2e284bdd9b | ||
|
|
22dc6ffcf3 | ||
|
|
54cd41c955 | ||
|
|
7cf8bd2dcf | ||
|
|
6a959b32fc | ||
|
|
3ce0c57e17 | ||
|
|
0fd11d16ee | ||
|
|
6ca5b7de03 | ||
|
|
c198f4ffa7 | ||
|
|
665bcab7b7 | ||
|
|
13afc0147e | ||
|
|
26db0dd1fc | ||
|
|
1f50421a54 | ||
|
|
1b3549f828 | ||
|
|
d635e7aadd | ||
|
|
201d2c6aac | ||
|
|
31074c3c8d | ||
|
|
2876a98a97 | ||
|
|
af4c74f1d9 | ||
|
|
e622ea1eba | ||
|
|
0269db0e85 | ||
|
|
859b575a12 | ||
|
|
f0b44ea93b | ||
|
|
802f648757 | ||
|
|
b834f6ebf9 | ||
|
|
6201257f76 | ||
|
|
670103356f | ||
|
|
a82278dcbf | ||
|
|
4845286e04 | ||
|
|
ad1ca78034 | ||
|
|
30b785c60d | ||
|
|
b4c5eb8354 | ||
|
|
91c3977d71 | ||
|
|
bb0b3c6470 | ||
|
|
8036f12114 | ||
|
|
457f183d46 | ||
|
|
eaf386f1d2 | ||
|
|
04182d1d44 | ||
|
|
5e360cf919 | ||
|
|
f371a00e98 | ||
|
|
e9f9f90b35 | ||
|
|
971f053f13 | ||
|
|
0482ec40fd | ||
|
|
f857625d07 | ||
|
|
01e7d79b1f | ||
|
|
d264a823d9 | ||
|
|
bfaaf2b8fa | ||
|
|
ca6d8c9dbe | ||
|
|
0e8337ba8e | ||
|
|
838a045239 | ||
|
|
33a0dc880f | ||
|
|
2d40c8d8c2 | ||
|
|
5d573b1b3e | ||
|
|
642aeeb937 | ||
|
|
e8e1cce14a | ||
|
|
6c8b207bf5 | ||
|
|
a183d50ad2 | ||
|
|
d26f2b44bd | ||
|
|
3bb587530b | ||
|
|
438ec8d245 | ||
|
|
bc05010ece | ||
|
|
0180d54dd9 | ||
|
|
45ea3c8b47 | ||
|
|
ef3aef9381 | ||
|
|
93c51c5aad | ||
|
|
6b21b4d87b | ||
|
|
9303a9c7d0 | ||
|
|
66c13b4a72 | ||
|
|
36fc643bfb | ||
|
|
0197600565 | ||
|
|
ed67dcc712 | ||
|
|
4b813ea1d1 | ||
|
|
4aa996c2db | ||
|
|
b1535d4751 | ||
|
|
71f0c01d4f | ||
|
|
f554fd90db | ||
|
|
e5a2642e62 | ||
|
|
8c2e756732 | ||
|
|
d8e98d1de6 | ||
|
|
dd8a64f30c | ||
|
|
3ce8da0126 | ||
|
|
30d7a5b01f | ||
|
|
7968394537 | ||
|
|
6a093abfdb | ||
|
|
d0b3cb32f0 | ||
|
|
5d028205bf | ||
|
|
0b2896458a | ||
|
|
e74f037732 | ||
|
|
ef7cb1e0cb | ||
|
|
49e43e59c4 | ||
|
|
996f60ff57 | ||
|
|
90c4de27e5 | ||
|
|
815e9e8e02 | ||
|
|
d1e71acf10 | ||
|
|
fc009969c9 | ||
|
|
63e8e7f842 | ||
|
|
210143738e | ||
|
|
deae380941 | ||
|
|
1fccff7e7c | ||
|
|
275e871618 | ||
|
|
c41bc65bf8 | ||
|
|
975dfe9489 | ||
|
|
5cf6438a45 | ||
|
|
05a8fffdbf | ||
|
|
c517565cc5 | ||
|
|
0a8ca19944 | ||
|
|
7c1eb5582a | ||
|
|
61bd9bb58c | ||
|
|
e2970fcf8a | ||
|
|
8b8b5174eb | ||
|
|
b30b96b9a1 | ||
|
|
dce2b1dfde | ||
|
|
258af94889 | ||
|
|
dabd7c0b27 | ||
|
|
f6786e29c6 | ||
|
|
eb852265ef | ||
|
|
b2f10359c8 | ||
|
|
3551912506 | ||
|
|
a87ee04024 | ||
|
|
cda563e672 | ||
|
|
a434e0540b | ||
|
|
d0992ef9bd | ||
|
|
c30cdb5411 | ||
|
|
b2d677256d | ||
|
|
a250f08b6c | ||
|
|
f44405207b | ||
|
|
feeac69197 | ||
|
|
2aa93d7d55 | ||
|
|
80c9e23282 | ||
|
|
366dee6575 | ||
|
|
44907503a4 | ||
|
|
b782892769 | ||
|
|
1974b34c29 | ||
|
|
6d51862057 | ||
|
|
d723c608d6 | ||
|
|
9a0de676c4 | ||
|
|
a555f99b1a | ||
|
|
32d3f760ec | ||
|
|
99581748e0 | ||
|
|
f0ae34d1ad | ||
|
|
4f39cf8abd | ||
|
|
e6df617cfc | ||
|
|
9d1d22565c | ||
|
|
fb507048f5 | ||
|
|
e7e6314146 | ||
|
|
649ca36ece | ||
|
|
253c8a342b | ||
|
|
71a9d51c56 | ||
|
|
ef164f5e69 | ||
|
|
b0484409d4 | ||
|
|
aa84709087 | ||
|
|
adf3eaf8fe | ||
|
|
4dcb8d2c2a | ||
|
|
2cf4518ffd | ||
|
|
3f29c68e92 | ||
|
|
a56dd1651b | ||
|
|
f32fc99e80 | ||
|
|
b5433720c1 | ||
|
|
613f4bb4c6 | ||
|
|
063f5303dd | ||
|
|
45caea4dbd | ||
|
|
68da8c23ad | ||
|
|
282896b653 | ||
|
|
3884dd974a | ||
|
|
64f20b580a | ||
|
|
e7cd8d41c6 | ||
|
|
f3e842fdd9 | ||
|
|
42954e84d9 | ||
|
|
0ff4223184 | ||
|
|
b618dbd97f | ||
|
|
213cffa9c5 | ||
|
|
dc2d2cdbde | ||
|
|
8b888fdf0c | ||
|
|
46c6db1739 | ||
|
|
11d93994c2 | ||
|
|
c78c0b73d3 | ||
|
|
2175f66a30 | ||
|
|
7029ef80f8 | ||
|
|
16d5ca3378 | ||
|
|
5b4e3434bb | ||
|
|
1dea38bdbb | ||
|
|
0fc4cd134f | ||
|
|
efffcc5f41 | ||
|
|
6f2ace538f | ||
|
|
50af3212f1 | ||
|
|
e2e91ef098 | ||
|
|
0c4a1b1489 | ||
|
|
61c3d6eb74 | ||
|
|
62876b9694 | ||
|
|
739dae0846 | ||
|
|
7ab67d205b | ||
|
|
224c9c5395 | ||
|
|
7904ce5a37 | ||
|
|
e770a05db0 | ||
|
|
89c89b4867 | ||
|
|
213ed6fa4b | ||
|
|
280689055f | ||
|
|
1d11cb468c | ||
|
|
05e080624b | ||
|
|
c6dc59f63e | ||
|
|
a0a85f62c6 | ||
|
|
3bb29e033b | ||
|
|
beee0c1e67 | ||
|
|
87c9c99259 | ||
|
|
b0b0bfdf76 | ||
|
|
669b0143af | ||
|
|
1a6bb2fedb | ||
|
|
b8767d13d5 | ||
|
|
686f82342b | ||
|
|
cfb8d25ff2 | ||
|
|
a24070c2b0 | ||
|
|
69e61398a6 | ||
|
|
89768ef003 | ||
|
|
794defe6fc | ||
|
|
27ba4eb978 | ||
|
|
3a0c2be885 | ||
|
|
04da679628 | ||
|
|
43be3fc409 | ||
|
|
567393a102 | ||
|
|
75851ee0b3 | ||
|
|
b54d7c37b6 | ||
|
|
69cf7b4d0d | ||
|
|
4f4ae44a16 | ||
|
|
31191bdfc8 | ||
|
|
091b376624 | ||
|
|
52fbe00c04 | ||
|
|
c94b7922aa | ||
|
|
8d82390843 | ||
|
|
0765b01189 | ||
|
|
b4f625e1ce | ||
|
|
b5b0f433ac | ||
|
|
bcbde3fdf0 | ||
|
|
1970e03361 | ||
|
|
07f99a946d | ||
|
|
89c6cda2ac | ||
|
|
6ea9581c6c | ||
|
|
18a8ed3e95 | ||
|
|
e2b1c97e26 | ||
|
|
6a809a2009 | ||
|
|
6b808d78e6 | ||
|
|
26edcdf6bc | ||
|
|
f77670c4dd | ||
|
|
5a64d67404 | ||
|
|
ceb5a3a04b | ||
|
|
999a3f62ac | ||
|
|
ca64b90cbf | ||
|
|
2a7f595d1c | ||
|
|
089bde3571 | ||
|
|
2f45183ee5 | ||
|
|
a5677e87db | ||
|
|
0d7b4bab11 | ||
|
|
2cf25b6c57 | ||
|
|
4fbb8ed994 | ||
|
|
4f78312a98 | ||
|
|
48832f8ff4 | ||
|
|
098d8e915d | ||
|
|
49e8918462 | ||
|
|
0f1e73facc | ||
|
|
f6bc0806d5 | ||
|
|
3bf9428cc0 | ||
|
|
ec32acec27 | ||
|
|
152b4f13e7 | ||
|
|
284ef5992d | ||
|
|
faf0e98665 | ||
|
|
4d614ed635 | ||
|
|
3653d7ed9a | ||
|
|
eb5e30579d | ||
|
|
1625e6c977 | ||
|
|
6af06348bd | ||
|
|
e960e45671 | ||
|
|
5bce87d6e7 | ||
|
|
7db6d51a39 | ||
|
|
b1510c43ff | ||
|
|
44c81910bf | ||
|
|
27e390965e | ||
|
|
119435ac76 | ||
|
|
b31c38a158 | ||
|
|
49ac3587b6 | ||
|
|
06da1d164d | ||
|
|
f2a4fd029d | ||
|
|
aa59c2f95f | ||
|
|
8d5a8085a4 | ||
|
|
083f27f19d | ||
|
|
fe0b9e9893 | ||
|
|
91bcc24977 | ||
|
|
d72b67c460 | ||
|
|
68b5afed06 | ||
|
|
c3e72b11db | ||
|
|
224f93f25d | ||
|
|
3af9f2f989 | ||
|
|
8668eff1b6 | ||
|
|
3cec57da6a | ||
|
|
6602123b55 | ||
|
|
767b4a2e2a | ||
|
|
beb96b7f01 | ||
|
|
4ede1174b7 | ||
|
|
e3a8bed5b2 | ||
|
|
4f214921e3 | ||
|
|
00932f4eb3 | ||
|
|
dbe87d6592 | ||
|
|
9416ee7224 | ||
|
|
75909a0add | ||
|
|
d9633dc20c | ||
|
|
75bee51ac1 | ||
|
|
7d203ed448 | ||
|
|
bb9da83e52 | ||
|
|
8e16e5d953 | ||
|
|
44180a68e6 | ||
|
|
cf5e2a1d20 | ||
|
|
ceece3dce4 | ||
|
|
520839a29b | ||
|
|
a6b1e4f5f7 | ||
|
|
c4df0d2690 | ||
|
|
3de77b7be9 | ||
|
|
f3a552ac9d | ||
|
|
27c0f783bc | ||
|
|
72a5d03f34 | ||
|
|
5b5dc3e442 | ||
|
|
1c7de24084 | ||
|
|
f157e2e637 | ||
|
|
f128be9b31 | ||
|
|
8a16ab52a9 | ||
|
|
b693a3ee47 | ||
|
|
f73aaef67a | ||
|
|
652152f167 | ||
|
|
6bab704bba | ||
|
|
fe5fe4c968 | ||
|
|
88bfa563df | ||
|
|
d60c3b7c0c | ||
|
|
73cad40fb7 | ||
|
|
cde3e87ad9 | ||
|
|
68a171f36c | ||
|
|
be2378a424 | ||
|
|
9aa2dacec9 | ||
|
|
ddf9301ceb | ||
|
|
7abe140621 | ||
|
|
ee62c52543 | ||
|
|
b4e4cbcbaa | ||
|
|
a7c3d1b8df | ||
|
|
79dede8e78 | ||
|
|
1d04d56bdb | ||
|
|
0d308e2b69 | ||
|
|
0c16a15b7d | ||
|
|
beebeaa265 | ||
|
|
c705a8ffc8 | ||
|
|
57c74e3f39 | ||
|
|
f92dd6bd16 | ||
|
|
99e1482e06 | ||
|
|
aec59a7e91 | ||
|
|
9a16805f17 | ||
|
|
2fd0c38ad1 | ||
|
|
2990703aeb | ||
|
|
acf2e30c2f | ||
|
|
5c5b7a33d3 | ||
|
|
e28ca0ae61 | ||
|
|
ad0630d04f | ||
|
|
4d362fe9ca | ||
|
|
bb77ab4a81 | ||
|
|
b42af99866 | ||
|
|
1a073629ec | ||
|
|
265522a2e3 | ||
|
|
c10be4b1cf | ||
|
|
065c931a83 | ||
|
|
7210ea36d2 | ||
|
|
5fd3eb2990 | ||
|
|
3a366dd0a6 | ||
|
|
1674bf1643 | ||
|
|
3e13ffda65 | ||
|
|
42ad8aec64 | ||
|
|
835d4c0aad | ||
|
|
ef9006e1a3 | ||
|
|
a8e2c8ef31 | ||
|
|
6f22f88d85 | ||
|
|
88a37067ad | ||
|
|
c6dab26769 | ||
|
|
9bceec9ef5 | ||
|
|
d1e491d57d | ||
|
|
7cfa1cc368 | ||
|
|
b79ee3c7f3 | ||
|
|
6085b21c36 | ||
|
|
b24d2e064a | ||
|
|
4fcb22c540 | ||
|
|
07689c9537 | ||
|
|
e329c7801d | ||
|
|
2de2df3a41 | ||
|
|
194a63cc71 | ||
|
|
6392405413 | ||
|
|
a18a4477e0 | ||
|
|
94ffeda62a | ||
|
|
264e81efb2 | ||
|
|
cea52a6db9 | ||
|
|
b8c7fcf587 | ||
|
|
04228791f3 | ||
|
|
04c4110f76 | ||
|
|
13e92cdb35 | ||
|
|
bd271b2367 | ||
|
|
9fea9f6fe0 | ||
|
|
8717cd0090 | ||
|
|
e487db349c | ||
|
|
29c0fe564c | ||
|
|
b2d1a1a17f | ||
|
|
4868918f94 | ||
|
|
15d0a116ac | ||
|
|
b5212d58ec | ||
|
|
cf1c577840 | ||
|
|
57127f39d0 | ||
|
|
052534de82 | ||
|
|
99e1eac094 | ||
|
|
7410bdb31c | ||
|
|
cc9c8d8ca1 | ||
|
|
5bacd2919d | ||
|
|
f55d18189c | ||
|
|
6097a0c9e1 | ||
|
|
39f8311484 | ||
|
|
fe8e437e74 | ||
|
|
1373708dea | ||
|
|
34605ee3c6 | ||
|
|
3444fb62f1 | ||
|
|
cc788cf44e | ||
|
|
13e4f607ad | ||
|
|
5da491c270 | ||
|
|
99fcc51019 | ||
|
|
350b9550c3 | ||
|
|
f289b281a0 | ||
|
|
9cd860d698 | ||
|
|
536b88790e | ||
|
|
73d3e8afd9 | ||
|
|
aef6bb9789 | ||
|
|
b4f56e92f3 | ||
|
|
8daa2c3703 | ||
|
|
c7f2a0ffdd | ||
|
|
c19360c6f2 | ||
|
|
71da6ab4e9 | ||
|
|
9f33685785 | ||
|
|
df4f1e7129 | ||
|
|
212182ee34 | ||
|
|
86988833e9 | ||
|
|
5a0504b5d9 | ||
|
|
e364680792 | ||
|
|
ef46070410 | ||
|
|
843b4b470b | ||
|
|
dcf6da2a51 | ||
|
|
9bb65ba9b7 | ||
|
|
3d29816ea3 | ||
|
|
2eeb2f52c5 | ||
|
|
32827be8b1 | ||
|
|
23c1f513e7 | ||
|
|
ae40bfb96e | ||
|
|
547f5ecfa6 | ||
|
|
1913f801b9 | ||
|
|
5842f7c837 | ||
|
|
1ef2eb6110 | ||
|
|
178625d3f2 | ||
|
|
9fb46a7b1c | ||
|
|
bea8de8a1a | ||
|
|
d68f6bbc42 | ||
|
|
7de999a7f9 | ||
|
|
ffb688b393 | ||
|
|
6aee6b0c46 | ||
|
|
aa78c9eaf5 | ||
|
|
02eda8943c | ||
|
|
2f7143e72f | ||
|
|
9ad14d991c | ||
|
|
59a6c9f13f | ||
|
|
ae838c12db | ||
|
|
ac9268bd48 | ||
|
|
0b58bebc90 | ||
|
|
93157e49d5 | ||
|
|
3d9f11168e | ||
|
|
c4cce147e2 | ||
|
|
3a2927e12e | ||
|
|
901ff9c3d5 | ||
|
|
8de3b26126 | ||
|
|
1e51ade620 | ||
|
|
89e6f93fa4 | ||
|
|
41630d6962 | ||
|
|
efcd0346fc | ||
|
|
f13deab812 | ||
|
|
f28b3c1269 | ||
|
|
5e1ec459e5 | ||
|
|
9a6f202c46 | ||
|
|
e11957ecf3 | ||
|
|
23c8809598 | ||
|
|
7098daaf72 | ||
|
|
ba73ab1c2a | ||
|
|
45f5fa6a74 | ||
|
|
50b7dbe7b2 | ||
|
|
811d532b48 | ||
|
|
7ceef826ac | ||
|
|
fe294dcb58 | ||
|
|
8441bda3da | ||
|
|
227900f288 | ||
|
|
345e0da76d | ||
|
|
139e12fa5f | ||
|
|
4df70c517d | ||
|
|
cbd6f7e1d0 | ||
|
|
426e55664f | ||
|
|
ab0a669662 | ||
|
|
5c03b20bd1 | ||
|
|
7eeebff874 | ||
|
|
a0a7372160 | ||
|
|
9e697d033f | ||
|
|
2ec3eaf68a | ||
|
|
e047872c25 | ||
|
|
f1173f8022 | ||
|
|
420286c103 | ||
|
|
1495f4881e | ||
|
|
452611242c | ||
|
|
d075172fd2 | ||
|
|
c32620b718 | ||
|
|
37b8d1ad10 | ||
|
|
be4f2aff63 | ||
|
|
14669dfbc5 | ||
|
|
5cec0d4bef | ||
|
|
dd74b57d13 | ||
|
|
a09bc6520f | ||
|
|
8a02ef1859 | ||
|
|
9ccd05ff54 | ||
|
|
42a8a0eb15 | ||
|
|
fb374ad99d | ||
|
|
a4f46d3310 | ||
|
|
5bc134ea7b | ||
|
|
28c289cfb8 | ||
|
|
0b5b27ea3a | ||
|
|
f1dbe3c9e2 | ||
|
|
9fb3201c8b | ||
|
|
96f8f99313 | ||
|
|
86c49f5e89 | ||
|
|
017dbb1d9f | ||
|
|
60eda52aba | ||
|
|
377fbced4a | ||
|
|
fb6a8da863 | ||
|
|
0cb5ba0f6e | ||
|
|
eb1b011989 | ||
|
|
72691b2e74 | ||
|
|
f9eeecf836 | ||
|
|
61e7936d79 | ||
|
|
4934fe5a8e | ||
|
|
2035398ef4 | ||
|
|
56160ef12b | ||
|
|
b0644ce18c | ||
|
|
cf5c3aa53b | ||
|
|
f9e87516ae | ||
|
|
ed1a30f67b | ||
|
|
77293b85e0 | ||
|
|
ad9210a7a7 | ||
|
|
15af61a7cc | ||
|
|
2d1417d72b | ||
|
|
41404424ac | ||
|
|
a2a531a747 | ||
|
|
a76c52ef84 | ||
|
|
ed75ef900a | ||
|
|
862502f3ed | ||
|
|
2b57b8371b | ||
|
|
4044b39ab7 | ||
|
|
e0bc56960b | ||
|
|
180352aa09 | ||
|
|
09a61f4706 | ||
|
|
5150251141 | ||
|
|
732d56b0c0 | ||
|
|
08aeb13804 | ||
|
|
1f0469894a | ||
|
|
8a0859fcba | ||
|
|
5212f57b5b | ||
|
|
6ac1301dc2 | ||
|
|
1eb0cde74f | ||
|
|
32f8fd4b9f | ||
|
|
90a5272860 | ||
|
|
40a8f0a243 | ||
|
|
f2ee9df600 | ||
|
|
9ab44b79ce | ||
|
|
50c11e105b | ||
|
|
b7c98ed433 | ||
|
|
d217b23719 | ||
|
|
171c6ab88f | ||
|
|
45411b1199 | ||
|
|
e0bfb13ed9 | ||
|
|
902332c5ae | ||
|
|
08d7211a93 | ||
|
|
1c0e204f50 | ||
|
|
d6e620a266 | ||
|
|
54b3d3abec | ||
|
|
31ed69a970 | ||
|
|
b1d9fd670a | ||
|
|
bf58511d81 | ||
|
|
b5317f3485 | ||
|
|
e805ffd945 | ||
|
|
40b5731198 | ||
|
|
ebd3d522e2 | ||
|
|
04179c5681 | ||
|
|
6c8ebdb456 | ||
|
|
85446c3b46 | ||
|
|
df7a68b709 | ||
|
|
1c65fd86ef | ||
|
|
bb00524f94 | ||
|
|
7f9e6bbf60 | ||
|
|
2ba21485f9 | ||
|
|
c998198aac | ||
|
|
3e8d4e4eb6 | ||
|
|
b15918b745 | ||
|
|
d2b7f18480 | ||
|
|
663b99d884 | ||
|
|
e3e18913f1 | ||
|
|
f98c454775 | ||
|
|
9c2944c090 | ||
|
|
66eb9095c1 | ||
|
|
e7c73518b3 | ||
|
|
172007f59c | ||
|
|
e262f8fb63 | ||
|
|
b6b3c04400 | ||
|
|
f97a3e0745 | ||
|
|
f9bccc83f9 | ||
|
|
761714036e | ||
|
|
d365d9d784 | ||
|
|
96f2985e86 | ||
|
|
7bad8e6ff3 | ||
|
|
55d53214a1 | ||
|
|
aee8398ee8 | ||
|
|
7cd63f2f8b | ||
|
|
8b5aafc4b4 | ||
|
|
f6323d94ec | ||
|
|
eb7ad07e31 | ||
|
|
7c7f87d201 | ||
|
|
18b94a2153 | ||
|
|
0b3e867362 | ||
|
|
3498273601 | ||
|
|
01a8c13ac0 | ||
|
|
194ba6009f | ||
|
|
c9c8acd029 | ||
|
|
7e2c06e301 | ||
|
|
40591cff25 | ||
|
|
5cf5168770 | ||
|
|
cb57dbf58a | ||
|
|
4b6df5d489 | ||
|
|
b6d29ccd30 | ||
|
|
16fb1e2078 | ||
|
|
ac00f7fee2 | ||
|
|
faeff029fa | ||
|
|
c6363aa146 | ||
|
|
2c70e53df6 | ||
|
|
81ab6aeeed | ||
|
|
d1a528eb07 | ||
|
|
c9c9f05e29 | ||
|
|
4e3e9ad5d4 | ||
|
|
3bd3d0285d | ||
|
|
343e678f09 | ||
|
|
b8adfcad87 | ||
|
|
ca3691e650 | ||
|
|
ad11d6e76b | ||
|
|
3e3216fb23 | ||
|
|
93cec9b3ee | ||
|
|
20c5c6b60c | ||
|
|
f29bb7d501 | ||
|
|
c3d9859c6e | ||
|
|
3b3b219a86 | ||
|
|
3f2e9c3592 | ||
|
|
787a3f8fcc | ||
|
|
a4cbf130b4 | ||
|
|
1c0e23db66 | ||
|
|
f33a1e2143 | ||
|
|
63ca9b2fe8 | ||
|
|
34f82fa1b1 | ||
|
|
5453bec1bf | ||
|
|
e65dd4fbc3 | ||
|
|
7b115b4b44 | ||
|
|
4d64f0e160 | ||
|
|
7453e96f5c | ||
|
|
9be9d3f580 | ||
|
|
7d51df4eed | ||
|
|
74fd904bc4 | ||
|
|
3c2a12d019 | ||
|
|
56f9aa41d0 | ||
|
|
e5d744f7d5 | ||
|
|
394676222f | ||
|
|
5464655bc2 | ||
|
|
768c3eb9a7 | ||
|
|
e316037910 | ||
|
|
97a08a1d99 | ||
|
|
db4420e10d | ||
|
|
480d4e6f4f | ||
|
|
0b701b3cf9 | ||
|
|
40216b5e7d | ||
|
|
b1fe07ac3d | ||
|
|
642de06fb5 | ||
|
|
7ad028fcb1 | ||
|
|
76385157fc | ||
|
|
c11f65720b | ||
|
|
5006fe2292 | ||
|
|
90fdb4889c | ||
|
|
c17a57d76c | ||
|
|
efcb749771 | ||
|
|
8c99a13387 | ||
|
|
79c1650c15 | ||
|
|
63ddabe5fb | ||
|
|
5448403345 | ||
|
|
5996149a8d | ||
|
|
62029e8a33 | ||
|
|
588e1711dd | ||
|
|
35c4a379ad | ||
|
|
cf62715499 | ||
|
|
1bfdff0e25 | ||
|
|
ba14b9d942 | ||
|
|
10fc7302eb | ||
|
|
3bea8f4adb | ||
|
|
24a36e6848 | ||
|
|
5d070cbf22 | ||
|
|
5e660977aa | ||
|
|
1f0600044a | ||
|
|
d3e3990d77 | ||
|
|
4a80f1e913 | ||
|
|
0550bdb467 | ||
|
|
5f07fa8057 | ||
|
|
10064fdac9 | ||
|
|
e7f6c780e2 | ||
|
|
e03b72df00 | ||
|
|
7d8b392fd9 | ||
|
|
afa8dc735d | ||
|
|
d49a74b927 | ||
|
|
ae36b3c832 | ||
|
|
e0c62e25b2 | ||
|
|
081958e282 | ||
|
|
379d5e3228 | ||
|
|
9b18601102 | ||
|
|
7868152ffe | ||
|
|
223cc1fb50 | ||
|
|
6ae9d19843 | ||
|
|
cbe4ff324c | ||
|
|
b8c06dd560 | ||
|
|
31d2cde6e5 | ||
|
|
75e2b404c8 | ||
|
|
d163dad3fe | ||
|
|
7271fdaaaa | ||
|
|
03fbb3c853 | ||
|
|
68c8bfa0e1 | ||
|
|
f34b47d29c | ||
|
|
a0b54ebedc | ||
|
|
ddb201db6c | ||
|
|
4069be3ff6 | ||
|
|
15f23eb045 | ||
|
|
77d17de14d | ||
|
|
bab08bf8f0 | ||
|
|
0d110ceb30 | ||
|
|
9cd3034762 | ||
|
|
ba51640c61 | ||
|
|
287593d75f | ||
|
|
3826e933c1 | ||
|
|
6b1020ec34 | ||
|
|
c8b22e71f0 | ||
|
|
41a5598103 | ||
|
|
fc92639d3f | ||
|
|
6587cd2478 | ||
|
|
e55c2ae554 | ||
|
|
693d6c0e5d | ||
|
|
952c1decf0 | ||
|
|
f3dbacabf8 | ||
|
|
819105c3aa | ||
|
|
06affc0700 | ||
|
|
f03d79c7d3 | ||
|
|
b52936bc5b | ||
|
|
3c4114091f | ||
|
|
d354aa1f62 | ||
|
|
45fd114143 | ||
|
|
32da5fea0a | ||
|
|
32c23c2410 | ||
|
|
802085f158 | ||
|
|
773c88cc04 | ||
|
|
197f51e50f | ||
|
|
1c66ce7dd7 | ||
|
|
8610a02d72 | ||
|
|
d7df86d6d0 | ||
|
|
e3d0f8f6e5 | ||
|
|
e8f2527fcd | ||
|
|
305dcb2b1e | ||
|
|
0ab352706b | ||
|
|
eac0bb4b37 | ||
|
|
3bc721720f | ||
|
|
54758b800b | ||
|
|
6cd6edce64 | ||
|
|
69a1d4d192 | ||
|
|
5d4864b82d | ||
|
|
3840aed5f7 | ||
|
|
6a629aec0c | ||
|
|
8b4bb72fb3 | ||
|
|
88d08c4f38 | ||
|
|
e3f5df3894 | ||
|
|
cfbd7d469f | ||
|
|
8fd7091068 | ||
|
|
9c97245c46 | ||
|
|
257aba4331 | ||
|
|
672e1b3575 | ||
|
|
ce50b0ed29 | ||
|
|
e88af874ca | ||
|
|
aed54439c2 | ||
|
|
c8c6e36fd9 | ||
|
|
e6ace84101 | ||
|
|
815b5e5f34 | ||
|
|
c149358028 | ||
|
|
b1265f6d70 | ||
|
|
a09e78873e | ||
|
|
17d8394ab6 | ||
|
|
e6a20aa44b | ||
|
|
458ca8a7ee | ||
|
|
b56e23eded | ||
|
|
d02c9c2150 | ||
|
|
cccb532a21 | ||
|
|
5c2bf7c822 | ||
|
|
e8758a77fb | ||
|
|
b2f0acfe26 | ||
|
|
8842d88058 | ||
|
|
739178d840 | ||
|
|
2454b3b644 | ||
|
|
87b243ed59 | ||
|
|
cec6f5db14 | ||
|
|
e7dd49b453 | ||
|
|
6fdc470b0b | ||
|
|
695aa7bec9 | ||
|
|
3952dba094 | ||
|
|
209f8155d1 | ||
|
|
8621733e17 | ||
|
|
621301862c | ||
|
|
fd4c5a90ed | ||
|
|
bfec073457 | ||
|
|
dc6e869c64 | ||
|
|
bed592460d | ||
|
|
46d3555798 | ||
|
|
74967737ee | ||
|
|
a347cb591d | ||
|
|
1ae60ef8a7 | ||
|
|
f9db687385 | ||
|
|
a38544796f | ||
|
|
5ecc8d1c71 | ||
|
|
c9cf7f6564 | ||
|
|
ba406d5747 | ||
|
|
c72ceadfce | ||
|
|
1e7f1b1e54 | ||
|
|
7c0f6f6e6e | ||
|
|
3f32cb154b | ||
|
|
6fa504489f | ||
|
|
433f373f60 | ||
|
|
b771b84f56 | ||
|
|
45abaec86e | ||
|
|
d0a96d463d | ||
|
|
12f1aedcd1 | ||
|
|
f0874bf585 | ||
|
|
1a749dc7b7 | ||
|
|
ec49d10007 | ||
|
|
294dc6fbd0 | ||
|
|
9e50a45b4c | ||
|
|
72ffb9dbb9 | ||
|
|
a677ab64e3 | ||
|
|
b33afcd47c | ||
|
|
659892edcb | ||
|
|
803fde6c1d | ||
|
|
83063a5740 | ||
|
|
7efa3a3ddf | ||
|
|
c1d93e251d | ||
|
|
8f3256675a | ||
|
|
9711ac2ecb | ||
|
|
833c56e17b | ||
|
|
9bb59c0f94 | ||
|
|
629ec6383e | ||
|
|
a6b6ecf4ba | ||
|
|
e5ce080fd3 | ||
|
|
80c4b2aa31 | ||
|
|
0b8f9edb9e | ||
|
|
63458a7de7 | ||
|
|
f6a080729a | ||
|
|
1dacddb7e3 | ||
|
|
334aa37b7f | ||
|
|
725f42e185 | ||
|
|
4910b35d69 | ||
|
|
69ce1e05f0 | ||
|
|
8d3136188d | ||
|
|
cb4f856ab6 | ||
|
|
65b269cd54 | ||
|
|
df93244373 | ||
|
|
a8de125e26 | ||
|
|
7db06b8afb | ||
|
|
7c6032cc84 | ||
|
|
939da455d7 | ||
|
|
817dbce391 | ||
|
|
0f447fadd4 | ||
|
|
79825dfa1d | ||
|
|
b640e9fe39 | ||
|
|
9dd4cbb40e | ||
|
|
82423db239 | ||
|
|
0262b64062 | ||
|
|
bd920b8ba9 | ||
|
|
57da86c598 | ||
|
|
cd92cc504b | ||
|
|
e82e584b81 | ||
|
|
ccbd8e8c70 | ||
|
|
e396261399 | ||
|
|
aae368f1e7 | ||
|
|
5ac4a852e1 | ||
|
|
fad33bc50f | ||
|
|
d5925b8ccf | ||
|
|
5e97a46afc | ||
|
|
8ee2134e05 | ||
|
|
2da9986717 | ||
|
|
c3b6b07e3a | ||
|
|
045d6ef1d0 | ||
|
|
ecba291ea0 | ||
|
|
328ab1cc27 | ||
|
|
884f84d5ed | ||
|
|
b2d48c86a6 | ||
|
|
3c5a0a3394 | ||
|
|
6ec82865d3 | ||
|
|
dab2225a39 | ||
|
|
0e93603aee | ||
|
|
d44f347b08 | ||
|
|
99f12ff720 | ||
|
|
a3cb3730b4 | ||
|
|
2cca3bf570 | ||
|
|
33cd261d83 | ||
|
|
e515d7bbbe | ||
|
|
0c05dc093f | ||
|
|
68701900d8 | ||
|
|
ee18d062ab | ||
|
|
695213dd31 | ||
|
|
50fc083d97 | ||
|
|
798fd84698 | ||
|
|
19575b2c8f | ||
|
|
a52f7f9baa | ||
|
|
fc84ffbd65 | ||
|
|
27e5d9672a | ||
|
|
8f31425daf | ||
|
|
7c4591617b | ||
|
|
c21ab943bb | ||
|
|
d8c8afe070 | ||
|
|
d1a35a5acc | ||
|
|
2d4cda7eda | ||
|
|
5e731d9557 | ||
|
|
6df7a80352 | ||
|
|
d261bbef88 | ||
|
|
4fa903a02f | ||
|
|
f0fd9e6137 | ||
|
|
a45ffce118 | ||
|
|
c558e7e0b1 | ||
|
|
bfec70481b | ||
|
|
a7c8fe4375 | ||
|
|
da52a5c9cf | ||
|
|
639cf1e038 | ||
|
|
76bfbad2c4 | ||
|
|
cca352fa9f | ||
|
|
7043ecc21b | ||
|
|
c384b8a81f | ||
|
|
747738c0fc | ||
|
|
8fc5b00d91 | ||
|
|
80b071024f | ||
|
|
3efd103712 | ||
|
|
3035cbc5db | ||
|
|
facdd586a3 | ||
|
|
a119a46495 | ||
|
|
0d3a50411e | ||
|
|
3bed84d712 | ||
|
|
0485f5c43d |
No files matched your search
@@ -14,6 +14,7 @@
|
|||||||
nbactions.xml
|
nbactions.xml
|
||||||
nb-configuration.xml
|
nb-configuration.xml
|
||||||
catalog.xml
|
catalog.xml
|
||||||
|
nbproject
|
||||||
|
|
||||||
# Compiled source #
|
# Compiled source #
|
||||||
###################
|
###################
|
||||||
|
|||||||
+3
-3
@@ -12,11 +12,11 @@ env:
|
|||||||
- MAVEN_SKIP_RC=true
|
- MAVEN_SKIP_RC=true
|
||||||
- MAVEN_OPTS="-Xms512m -Xmx2048m"
|
- MAVEN_OPTS="-Xms512m -Xmx2048m"
|
||||||
matrix:
|
matrix:
|
||||||
- TESTS=old
|
|
||||||
- TESTS=group1
|
- TESTS=group1
|
||||||
- TESTS=group2
|
- TESTS=group2
|
||||||
- TESTS=group3
|
- TESTS=group3
|
||||||
- TESTS=adapter
|
- TESTS=group4
|
||||||
|
- TESTS=old
|
||||||
|
|
||||||
jdk:
|
jdk:
|
||||||
- oraclejdk8
|
- oraclejdk8
|
||||||
@@ -25,7 +25,7 @@ before_script:
|
|||||||
- export MAVEN_SKIP_RC=true
|
- export MAVEN_SKIP_RC=true
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- travis_wait 60 mvn install -Pdistribution -DskipTests=true -B -V -q
|
- travis_wait 60 mvn install --no-snapshot-updates -Pdistribution -DskipTestsuite -B -V -q
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- ./travis-run-tests.sh $TESTS
|
- ./travis-run-tests.sh $TESTS
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ To start Keycloak during development first build as specified above, then run:
|
|||||||
mvn -f testsuite/integration/pom.xml exec:java -Pkeycloak-server
|
mvn -f testsuite/integration/pom.xml exec:java -Pkeycloak-server
|
||||||
|
|
||||||
|
|
||||||
To start Keycloak from the appliance distribution first build the distribution it as specified above, then run:
|
To start Keycloak from the server distribution first build the distribution it as specified above, then run:
|
||||||
|
|
||||||
tar xfz distribution/appliance-dist/target/keycloak-appliance-dist-all-<VERSION>.tar.gz
|
tar xfz distribution/server-dist/target/keycloak-<VERSION>.tar.gz
|
||||||
cd keycloak-appliance-dist-all-<VERSION>/keycloak
|
cd keycloak-<VERSION>
|
||||||
bin/standalone.sh
|
bin/standalone.sh
|
||||||
|
|
||||||
To stop the server press `Ctrl + C`.
|
To stop the server press `Ctrl + C`.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+16
@@ -342,6 +342,12 @@ public class AdapterDeploymentContext {
|
|||||||
return delegate.getNotBefore();
|
return delegate.getNotBefore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateNotBefore(int notBefore) {
|
||||||
|
delegate.setNotBefore(notBefore);
|
||||||
|
getPublicKeyLocator().reset(this);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setExposeToken(boolean exposeToken) {
|
public void setExposeToken(boolean exposeToken) {
|
||||||
delegate.setExposeToken(exposeToken);
|
delegate.setExposeToken(exposeToken);
|
||||||
@@ -446,6 +452,16 @@ public class AdapterDeploymentContext {
|
|||||||
public int getMinTimeBetweenJwksRequests() {
|
public int getMinTimeBetweenJwksRequests() {
|
||||||
return delegate.getMinTimeBetweenJwksRequests();
|
return delegate.getMinTimeBetweenJwksRequests();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPublicKeyCacheTtl() {
|
||||||
|
return delegate.getPublicKeyCacheTtl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPublicKeyCacheTtl(int publicKeyCacheTtl) {
|
||||||
|
delegate.setPublicKeyCacheTtl(publicKeyCacheTtl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected KeycloakUriBuilder getBaseBuilder(HttpFacade facade, String base) {
|
protected KeycloakUriBuilder getBaseBuilder(HttpFacade facade, String base) {
|
||||||
|
|||||||
+4
@@ -101,6 +101,7 @@ public class AuthenticatedActionsHandler {
|
|||||||
if (!deployment.isCors()) return false;
|
if (!deployment.isCors()) return false;
|
||||||
KeycloakSecurityContext securityContext = facade.getSecurityContext();
|
KeycloakSecurityContext securityContext = facade.getSecurityContext();
|
||||||
String origin = facade.getRequest().getHeader(CorsHeaders.ORIGIN);
|
String origin = facade.getRequest().getHeader(CorsHeaders.ORIGIN);
|
||||||
|
String exposeHeaders = deployment.getCorsExposedHeaders();
|
||||||
String requestOrigin = UriUtils.getOrigin(facade.getRequest().getURI());
|
String requestOrigin = UriUtils.getOrigin(facade.getRequest().getURI());
|
||||||
log.debugv("Origin: {0} uri: {1}", origin, facade.getRequest().getURI());
|
log.debugv("Origin: {0} uri: {1}", origin, facade.getRequest().getURI());
|
||||||
if (securityContext != null && origin != null && !origin.equals(requestOrigin)) {
|
if (securityContext != null && origin != null && !origin.equals(requestOrigin)) {
|
||||||
@@ -124,6 +125,9 @@ public class AuthenticatedActionsHandler {
|
|||||||
facade.getResponse().setStatus(200);
|
facade.getResponse().setStatus(200);
|
||||||
facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
|
facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
|
||||||
facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
||||||
|
if (exposeHeaders != null) {
|
||||||
|
facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, exposeHeaders);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.debugv("cors validation not needed as we're not a secure session or origin header was null: {0}", facade.getRequest().getURI());
|
log.debugv("cors validation not needed as we're not a secure session or origin header was null: {0}", facade.getRequest().getURI());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,4 +30,5 @@ public interface CorsHeaders {
|
|||||||
String ORIGIN = "Origin";
|
String ORIGIN = "Origin";
|
||||||
String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
|
String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
|
||||||
String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
|
String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
|
||||||
|
String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers";
|
||||||
}
|
}
|
||||||
+1
-8
@@ -46,14 +46,7 @@ public class HttpAdapterUtils {
|
|||||||
}
|
}
|
||||||
InputStream is = entity.getContent();
|
InputStream is = entity.getContent();
|
||||||
try {
|
try {
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
return JsonSerialization.readValue(is, clazz);
|
||||||
int c;
|
|
||||||
while ((c = is.read()) != -1) {
|
|
||||||
os.write(c);
|
|
||||||
}
|
|
||||||
byte[] bytes = os.toByteArray();
|
|
||||||
String json = new String(bytes);
|
|
||||||
return JsonSerialization.readValue(json, clazz);
|
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
is.close();
|
is.close();
|
||||||
|
|||||||
+3
-3
@@ -170,8 +170,8 @@ public class HttpClientBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpClientBuilder disableCookieCache() {
|
public HttpClientBuilder disableCookieCache(boolean disable) {
|
||||||
this.disableCookieCache = true;
|
this.disableCookieCache = disable;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +334,7 @@ public class HttpClientBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public HttpClient build(AdapterHttpClientConfig adapterConfig) {
|
public HttpClient build(AdapterHttpClientConfig adapterConfig) {
|
||||||
disableCookieCache(); // disable cookie cache as we don't want sticky sessions for load balancing
|
disableCookieCache(true); // disable cookie cache as we don't want sticky sessions for load balancing
|
||||||
|
|
||||||
String truststorePath = adapterConfig.getTruststore();
|
String truststorePath = adapterConfig.getTruststore();
|
||||||
if (truststorePath != null) {
|
if (truststorePath != null) {
|
||||||
|
|||||||
+67
@@ -35,6 +35,8 @@ import java.util.Map;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @author <a href="mailto:brad.culley@spartasystems.com">Brad Culley</a>
|
||||||
|
* @author <a href="mailto:john.ament@spartasystems.com">John D. Ament</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class KeycloakDeployment {
|
public class KeycloakDeployment {
|
||||||
@@ -57,6 +59,7 @@ public class KeycloakDeployment {
|
|||||||
|
|
||||||
protected String resourceName;
|
protected String resourceName;
|
||||||
protected boolean bearerOnly;
|
protected boolean bearerOnly;
|
||||||
|
protected boolean autodetectBearerOnly;
|
||||||
protected boolean enableBasicAuth;
|
protected boolean enableBasicAuth;
|
||||||
protected boolean publicClient;
|
protected boolean publicClient;
|
||||||
protected Map<String, Object> resourceCredentials = new HashMap<>();
|
protected Map<String, Object> resourceCredentials = new HashMap<>();
|
||||||
@@ -72,6 +75,7 @@ public class KeycloakDeployment {
|
|||||||
protected int corsMaxAge = -1;
|
protected int corsMaxAge = -1;
|
||||||
protected String corsAllowedHeaders;
|
protected String corsAllowedHeaders;
|
||||||
protected String corsAllowedMethods;
|
protected String corsAllowedMethods;
|
||||||
|
protected String corsExposedHeaders;
|
||||||
protected boolean exposeToken;
|
protected boolean exposeToken;
|
||||||
protected boolean alwaysRefreshToken;
|
protected boolean alwaysRefreshToken;
|
||||||
protected boolean registerNodeAtStartup;
|
protected boolean registerNodeAtStartup;
|
||||||
@@ -81,8 +85,15 @@ public class KeycloakDeployment {
|
|||||||
protected volatile int notBefore;
|
protected volatile int notBefore;
|
||||||
protected int tokenMinimumTimeToLive;
|
protected int tokenMinimumTimeToLive;
|
||||||
protected int minTimeBetweenJwksRequests;
|
protected int minTimeBetweenJwksRequests;
|
||||||
|
protected int publicKeyCacheTtl;
|
||||||
private PolicyEnforcer policyEnforcer;
|
private PolicyEnforcer policyEnforcer;
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc7636
|
||||||
|
protected boolean pkce = false;
|
||||||
|
protected boolean ignoreOAuthQueryParameter;
|
||||||
|
|
||||||
|
protected Map<String, String> redirectRewriteRules;
|
||||||
|
|
||||||
public KeycloakDeployment() {
|
public KeycloakDeployment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +211,14 @@ public class KeycloakDeployment {
|
|||||||
this.bearerOnly = bearerOnly;
|
this.bearerOnly = bearerOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isAutodetectBearerOnly() {
|
||||||
|
return autodetectBearerOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAutodetectBearerOnly(boolean autodetectBearerOnly) {
|
||||||
|
this.autodetectBearerOnly = autodetectBearerOnly;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEnableBasicAuth() {
|
public boolean isEnableBasicAuth() {
|
||||||
return enableBasicAuth;
|
return enableBasicAuth;
|
||||||
}
|
}
|
||||||
@@ -312,6 +331,14 @@ public class KeycloakDeployment {
|
|||||||
this.corsAllowedMethods = corsAllowedMethods;
|
this.corsAllowedMethods = corsAllowedMethods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getCorsExposedHeaders() {
|
||||||
|
return corsExposedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCorsExposedHeaders(String corsExposedHeaders) {
|
||||||
|
this.corsExposedHeaders = corsExposedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isExposeToken() {
|
public boolean isExposeToken() {
|
||||||
return exposeToken;
|
return exposeToken;
|
||||||
}
|
}
|
||||||
@@ -328,6 +355,11 @@ public class KeycloakDeployment {
|
|||||||
this.notBefore = notBefore;
|
this.notBefore = notBefore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateNotBefore(int notBefore) {
|
||||||
|
this.notBefore = notBefore;
|
||||||
|
getPublicKeyLocator().reset(this);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isAlwaysRefreshToken() {
|
public boolean isAlwaysRefreshToken() {
|
||||||
return alwaysRefreshToken;
|
return alwaysRefreshToken;
|
||||||
}
|
}
|
||||||
@@ -384,6 +416,14 @@ public class KeycloakDeployment {
|
|||||||
this.minTimeBetweenJwksRequests = minTimeBetweenJwksRequests;
|
this.minTimeBetweenJwksRequests = minTimeBetweenJwksRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getPublicKeyCacheTtl() {
|
||||||
|
return publicKeyCacheTtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPublicKeyCacheTtl(int publicKeyCacheTtl) {
|
||||||
|
this.publicKeyCacheTtl = publicKeyCacheTtl;
|
||||||
|
}
|
||||||
|
|
||||||
public void setPolicyEnforcer(PolicyEnforcer policyEnforcer) {
|
public void setPolicyEnforcer(PolicyEnforcer policyEnforcer) {
|
||||||
this.policyEnforcer = policyEnforcer;
|
this.policyEnforcer = policyEnforcer;
|
||||||
}
|
}
|
||||||
@@ -391,4 +431,31 @@ public class KeycloakDeployment {
|
|||||||
public PolicyEnforcer getPolicyEnforcer() {
|
public PolicyEnforcer getPolicyEnforcer() {
|
||||||
return policyEnforcer;
|
return policyEnforcer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc7636
|
||||||
|
public boolean isPkce() {
|
||||||
|
return pkce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPkce(boolean pkce) {
|
||||||
|
this.pkce = pkce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIgnoreOAuthQueryParameter(boolean ignoreOAuthQueryParameter) {
|
||||||
|
this.ignoreOAuthQueryParameter = ignoreOAuthQueryParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOAuthQueryParameterEnabled() {
|
||||||
|
return !this.ignoreOAuthQueryParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getRedirectRewriteRules() {
|
||||||
|
return redirectRewriteRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRewriteRedirectRules(Map<String, String> redirectRewriteRules) {
|
||||||
|
this.redirectRewriteRules = redirectRewriteRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
+12
@@ -37,6 +37,8 @@ import java.security.PublicKey;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @author <a href="mailto:brad.culley@spartasystems.com">Brad Culley</a>
|
||||||
|
* @author <a href="mailto:john.ament@spartasystems.com">John D. Ament</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class KeycloakDeploymentBuilder {
|
public class KeycloakDeploymentBuilder {
|
||||||
@@ -96,15 +98,25 @@ public class KeycloakDeploymentBuilder {
|
|||||||
deployment.setCorsMaxAge(adapterConfig.getCorsMaxAge());
|
deployment.setCorsMaxAge(adapterConfig.getCorsMaxAge());
|
||||||
deployment.setCorsAllowedHeaders(adapterConfig.getCorsAllowedHeaders());
|
deployment.setCorsAllowedHeaders(adapterConfig.getCorsAllowedHeaders());
|
||||||
deployment.setCorsAllowedMethods(adapterConfig.getCorsAllowedMethods());
|
deployment.setCorsAllowedMethods(adapterConfig.getCorsAllowedMethods());
|
||||||
|
deployment.setCorsExposedHeaders(adapterConfig.getCorsExposedHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc7636
|
||||||
|
if (adapterConfig.isPkce()) {
|
||||||
|
deployment.setPkce(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
deployment.setBearerOnly(adapterConfig.isBearerOnly());
|
deployment.setBearerOnly(adapterConfig.isBearerOnly());
|
||||||
|
deployment.setAutodetectBearerOnly(adapterConfig.isAutodetectBearerOnly());
|
||||||
deployment.setEnableBasicAuth(adapterConfig.isEnableBasicAuth());
|
deployment.setEnableBasicAuth(adapterConfig.isEnableBasicAuth());
|
||||||
deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken());
|
deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken());
|
||||||
deployment.setRegisterNodeAtStartup(adapterConfig.isRegisterNodeAtStartup());
|
deployment.setRegisterNodeAtStartup(adapterConfig.isRegisterNodeAtStartup());
|
||||||
deployment.setRegisterNodePeriod(adapterConfig.getRegisterNodePeriod());
|
deployment.setRegisterNodePeriod(adapterConfig.getRegisterNodePeriod());
|
||||||
deployment.setTokenMinimumTimeToLive(adapterConfig.getTokenMinimumTimeToLive());
|
deployment.setTokenMinimumTimeToLive(adapterConfig.getTokenMinimumTimeToLive());
|
||||||
deployment.setMinTimeBetweenJwksRequests(adapterConfig.getMinTimeBetweenJwksRequests());
|
deployment.setMinTimeBetweenJwksRequests(adapterConfig.getMinTimeBetweenJwksRequests());
|
||||||
|
deployment.setPublicKeyCacheTtl(adapterConfig.getPublicKeyCacheTtl());
|
||||||
|
deployment.setIgnoreOAuthQueryParameter(adapterConfig.isIgnoreOAuthQueryParameter());
|
||||||
|
deployment.setRewriteRedirectRules(adapterConfig.getRedirectRewriteRules());
|
||||||
|
|
||||||
if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) {
|
if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) {
|
||||||
throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url");
|
throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url");
|
||||||
|
|||||||
+29
-9
@@ -37,7 +37,10 @@ import org.keycloak.representations.IDToken;
|
|||||||
import org.keycloak.util.TokenUtil;
|
import org.keycloak.util.TokenUtil;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,6 +143,7 @@ public class OAuthRequestAuthenticator {
|
|||||||
protected String getRedirectUri(String state) {
|
protected String getRedirectUri(String state) {
|
||||||
String url = getRequestUrl();
|
String url = getRequestUrl();
|
||||||
log.debugf("callback uri: %s", url);
|
log.debugf("callback uri: %s", url);
|
||||||
|
|
||||||
if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
|
if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
|
||||||
int port = sslRedirectPort();
|
int port = sslRedirectPort();
|
||||||
if (port < 0) {
|
if (port < 0) {
|
||||||
@@ -169,7 +173,7 @@ public class OAuthRequestAuthenticator {
|
|||||||
KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone()
|
KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone()
|
||||||
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
||||||
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
|
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
|
||||||
.queryParam(OAuth2Constants.REDIRECT_URI, url)
|
.queryParam(OAuth2Constants.REDIRECT_URI, rewrittenRedirectUri(url))
|
||||||
.queryParam(OAuth2Constants.STATE, state)
|
.queryParam(OAuth2Constants.STATE, state)
|
||||||
.queryParam("login", "true");
|
.queryParam("login", "true");
|
||||||
if(loginHint != null && loginHint.length() > 0){
|
if(loginHint != null && loginHint.length() > 0){
|
||||||
@@ -195,15 +199,13 @@ public class OAuthRequestAuthenticator {
|
|||||||
return sslRedirectPort;
|
return sslRedirectPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static final AtomicLong counter = new AtomicLong();
|
|
||||||
|
|
||||||
protected String getStateCode() {
|
protected String getStateCode() {
|
||||||
return counter.getAndIncrement() + "/" + AdapterUtils.generateId();
|
return AdapterUtils.generateId();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected AuthChallenge loginRedirect() {
|
protected AuthChallenge loginRedirect() {
|
||||||
final String state = getStateCode();
|
final String state = getStateCode();
|
||||||
final String redirect = getRedirectUri(state);
|
final String redirect = getRedirectUri(state);
|
||||||
if (redirect == null) {
|
if (redirect == null) {
|
||||||
return challenge(403, OIDCAuthenticationError.Reason.NO_REDIRECT_URI, null);
|
return challenge(403, OIDCAuthenticationError.Reason.NO_REDIRECT_URI, null);
|
||||||
}
|
}
|
||||||
@@ -321,10 +323,11 @@ public class OAuthRequestAuthenticator {
|
|||||||
|
|
||||||
AccessTokenResponse tokenResponse = null;
|
AccessTokenResponse tokenResponse = null;
|
||||||
strippedOauthParametersRequestUri = stripOauthParametersFromRedirect();
|
strippedOauthParametersRequestUri = stripOauthParametersFromRedirect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// For COOKIE store we don't have httpSessionId and single sign-out won't be available
|
// For COOKIE store we don't have httpSessionId and single sign-out won't be available
|
||||||
String httpSessionId = deployment.getTokenStore() == TokenStore.SESSION ? reqAuthenticator.changeHttpSessionId(true) : null;
|
String httpSessionId = deployment.getTokenStore() == TokenStore.SESSION ? reqAuthenticator.changeHttpSessionId(true) : null;
|
||||||
tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, strippedOauthParametersRequestUri, httpSessionId);
|
tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, rewrittenRedirectUri(strippedOauthParametersRequestUri), httpSessionId);
|
||||||
} catch (ServerRequest.HttpFailure failure) {
|
} catch (ServerRequest.HttpFailure failure) {
|
||||||
log.error("failed to turn code into token");
|
log.error("failed to turn code into token");
|
||||||
log.error("status from server: " + failure.getStatus());
|
log.error("status from server: " + failure.getStatus());
|
||||||
@@ -357,7 +360,7 @@ public class OAuthRequestAuthenticator {
|
|||||||
return challenge(403, OIDCAuthenticationError.Reason.INVALID_TOKEN, null);
|
return challenge(403, OIDCAuthenticationError.Reason.INVALID_TOKEN, null);
|
||||||
}
|
}
|
||||||
if (tokenResponse.getNotBeforePolicy() > deployment.getNotBefore()) {
|
if (tokenResponse.getNotBeforePolicy() > deployment.getNotBefore()) {
|
||||||
deployment.setNotBefore(tokenResponse.getNotBeforePolicy());
|
deployment.updateNotBefore(tokenResponse.getNotBeforePolicy());
|
||||||
}
|
}
|
||||||
if (token.getIssuedAt() < deployment.getNotBefore()) {
|
if (token.getIssuedAt() < deployment.getNotBefore()) {
|
||||||
log.error("Stale token");
|
log.error("Stale token");
|
||||||
@@ -376,6 +379,23 @@ public class OAuthRequestAuthenticator {
|
|||||||
.replaceQueryParam(OAuth2Constants.STATE, null);
|
.replaceQueryParam(OAuth2Constants.STATE, null);
|
||||||
return builder.build().toString();
|
return builder.build().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String rewrittenRedirectUri(String originalUri) {
|
||||||
|
Map<String, String> rewriteRules = deployment.getRedirectRewriteRules();
|
||||||
|
if(rewriteRules != null && !rewriteRules.isEmpty()) {
|
||||||
|
try {
|
||||||
|
URL url = new URL(originalUri);
|
||||||
|
Map.Entry<String, String> rule = rewriteRules.entrySet().iterator().next();
|
||||||
|
StringBuilder redirectUriBuilder = new StringBuilder(url.getProtocol());
|
||||||
|
redirectUriBuilder.append("://"+ url.getAuthority());
|
||||||
|
redirectUriBuilder.append(url.getPath().replaceFirst(rule.getKey(), rule.getValue()));
|
||||||
|
return redirectUriBuilder.toString();
|
||||||
|
} catch (MalformedURLException ex) {
|
||||||
|
log.error("Not a valid request url");
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return originalUri;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+2
-2
@@ -155,7 +155,7 @@ public class PreAuthActionsHandler {
|
|||||||
} else {
|
} else {
|
||||||
log.debugf("logout of all sessions for application '%s'", action.getResource());
|
log.debugf("logout of all sessions for application '%s'", action.getResource());
|
||||||
if (action.getNotBefore() > deployment.getNotBefore()) {
|
if (action.getNotBefore() > deployment.getNotBefore()) {
|
||||||
deployment.setNotBefore(action.getNotBefore());
|
deployment.updateNotBefore(action.getNotBefore());
|
||||||
}
|
}
|
||||||
userSessionManagement.logoutAll();
|
userSessionManagement.logoutAll();
|
||||||
}
|
}
|
||||||
@@ -177,7 +177,7 @@ public class PreAuthActionsHandler {
|
|||||||
}
|
}
|
||||||
PushNotBeforeAction action = JsonSerialization.readValue(token.getContent(), PushNotBeforeAction.class);
|
PushNotBeforeAction action = JsonSerialization.readValue(token.getContent(), PushNotBeforeAction.class);
|
||||||
if (!validateAction(action)) return;
|
if (!validateAction(action)) return;
|
||||||
deployment.setNotBefore(action.getNotBefore());
|
deployment.updateNotBefore(action.getNotBefore());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -22,6 +22,8 @@ import org.keycloak.adapters.spi.HttpFacade;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:froehlich.ch@gmail.com">Christian Froehlich</a>
|
* @author <a href="mailto:froehlich.ch@gmail.com">Christian Froehlich</a>
|
||||||
|
* @author <a href="mailto:brad.culley@spartasystems.com">Brad Culley</a>
|
||||||
|
* @author <a href="mailto:john.ament@spartasystems.com">John D. Ament</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class QueryParamterTokenRequestAuthenticator extends BearerTokenRequestAuthenticator {
|
public class QueryParamterTokenRequestAuthenticator extends BearerTokenRequestAuthenticator {
|
||||||
@@ -33,6 +35,9 @@ public class QueryParamterTokenRequestAuthenticator extends BearerTokenRequestAu
|
|||||||
}
|
}
|
||||||
|
|
||||||
public AuthOutcome authenticate(HttpFacade exchange) {
|
public AuthOutcome authenticate(HttpFacade exchange) {
|
||||||
|
if(!deployment.isOAuthQueryParameterEnabled()) {
|
||||||
|
return AuthOutcome.NOT_ATTEMPTED;
|
||||||
|
}
|
||||||
tokenString = null;
|
tokenString = null;
|
||||||
tokenString = getAccessTokenFromQueryParamter(exchange);
|
tokenString = getAccessTokenFromQueryParamter(exchange);
|
||||||
if (tokenString == null || tokenString.trim().isEmpty()) {
|
if (tokenString == null || tokenString.trim().isEmpty()) {
|
||||||
|
|||||||
+1
-1
@@ -144,7 +144,7 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response.getNotBeforePolicy() > deployment.getNotBefore()) {
|
if (response.getNotBeforePolicy() > deployment.getNotBefore()) {
|
||||||
deployment.setNotBefore(response.getNotBeforePolicy());
|
deployment.updateNotBefore(response.getNotBeforePolicy());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
|||||||
+41
-1
@@ -17,11 +17,14 @@
|
|||||||
|
|
||||||
package org.keycloak.adapters;
|
package org.keycloak.adapters;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.KeycloakPrincipal;
|
import org.keycloak.KeycloakPrincipal;
|
||||||
import org.keycloak.adapters.spi.AuthChallenge;
|
import org.keycloak.adapters.spi.AuthChallenge;
|
||||||
import org.keycloak.adapters.spi.AuthOutcome;
|
import org.keycloak.adapters.spi.AuthOutcome;
|
||||||
import org.keycloak.adapters.spi.HttpFacade;
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.common.enums.SslRequired;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
@@ -116,6 +119,12 @@ public abstract class RequestAuthenticator {
|
|||||||
return AuthOutcome.NOT_ATTEMPTED;
|
return AuthOutcome.NOT_ATTEMPTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAutodetectedBearerOnly(facade.getRequest())) {
|
||||||
|
challenge = bearer.getChallenge();
|
||||||
|
log.debug("NOT_ATTEMPTED: Treating as bearer only");
|
||||||
|
return AuthOutcome.NOT_ATTEMPTED;
|
||||||
|
}
|
||||||
|
|
||||||
if (log.isTraceEnabled()) {
|
if (log.isTraceEnabled()) {
|
||||||
log.trace("try oauth");
|
log.trace("try oauth");
|
||||||
}
|
}
|
||||||
@@ -152,12 +161,43 @@ public abstract class RequestAuthenticator {
|
|||||||
|
|
||||||
protected boolean verifySSL() {
|
protected boolean verifySSL() {
|
||||||
if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
|
if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
|
||||||
log.warn("SSL is required to authenticate");
|
log.warnf("SSL is required to authenticate. Remote address %s is secure: %s, SSL required for: %s .",
|
||||||
|
facade.getRequest().getRemoteAddr(), facade.getRequest().isSecure(), deployment.getSslRequired().name());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean isAutodetectedBearerOnly(HttpFacade.Request request) {
|
||||||
|
if (!deployment.isAutodetectBearerOnly()) return false;
|
||||||
|
|
||||||
|
String headerValue = facade.getRequest().getHeader("X-Requested-With");
|
||||||
|
if (headerValue != null && headerValue.equalsIgnoreCase("XMLHttpRequest")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
headerValue = facade.getRequest().getHeader("Faces-Request");
|
||||||
|
if (headerValue != null && headerValue.startsWith("partial/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
headerValue = facade.getRequest().getHeader("SOAPAction");
|
||||||
|
if (headerValue != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> accepts = facade.getRequest().getHeaders("Accept");
|
||||||
|
if (accepts == null) accepts = Collections.emptyList();
|
||||||
|
|
||||||
|
for (String accept : accepts) {
|
||||||
|
if (accept.contains("text/html") || accept.contains("text/*") || accept.contains("*/*")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract OAuthRequestAuthenticator createOAuthAuthenticator();
|
protected abstract OAuthRequestAuthenticator createOAuthAuthenticator();
|
||||||
|
|
||||||
protected BearerTokenRequestAuthenticator createBearerTokenAuthenticator() {
|
protected BearerTokenRequestAuthenticator createBearerTokenAuthenticator() {
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import org.keycloak.constants.AdapterConstants;
|
|||||||
import org.keycloak.representations.AccessTokenResponse;
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -46,6 +48,8 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public class ServerRequest {
|
public class ServerRequest {
|
||||||
|
|
||||||
|
private static Logger logger = Logger.getLogger(ServerRequest.class);
|
||||||
|
|
||||||
public static class HttpFailure extends Exception {
|
public static class HttpFailure extends Exception {
|
||||||
private int status;
|
private int status;
|
||||||
private String error;
|
private String error;
|
||||||
@@ -136,6 +140,62 @@ public class ServerRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc7636#section-4
|
||||||
|
public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri, String sessionId, String codeVerifier) throws IOException, HttpFailure {
|
||||||
|
List<NameValuePair> formparams = new ArrayList<>();
|
||||||
|
redirectUri = stripOauthParametersFromRedirect(redirectUri);
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "authorization_code"));
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, redirectUri));
|
||||||
|
if (sessionId != null) {
|
||||||
|
formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, sessionId));
|
||||||
|
formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, HostUtils.getHostName()));
|
||||||
|
}
|
||||||
|
// https://tools.ietf.org/html/rfc7636#section-4
|
||||||
|
if (codeVerifier != null) {
|
||||||
|
logger.debugf("add to POST parameters of Token Request, codeVerifier = %s", codeVerifier);
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.CODE_VERIFIER, codeVerifier));
|
||||||
|
} else {
|
||||||
|
logger.debug("add to POST parameters of Token Request without codeVerifier");
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpPost post = new HttpPost(deployment.getTokenUrl());
|
||||||
|
ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
|
||||||
|
|
||||||
|
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
|
||||||
|
post.setEntity(form);
|
||||||
|
HttpResponse response = deployment.getClient().execute(post);
|
||||||
|
int status = response.getStatusLine().getStatusCode();
|
||||||
|
HttpEntity entity = response.getEntity();
|
||||||
|
if (status != 200) {
|
||||||
|
error(status, entity);
|
||||||
|
}
|
||||||
|
if (entity == null) {
|
||||||
|
throw new HttpFailure(status, null);
|
||||||
|
}
|
||||||
|
InputStream is = entity.getContent();
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
|
int c;
|
||||||
|
while ((c = is.read()) != -1) {
|
||||||
|
os.write(c);
|
||||||
|
}
|
||||||
|
byte[] bytes = os.toByteArray();
|
||||||
|
String json = new String(bytes);
|
||||||
|
try {
|
||||||
|
return JsonSerialization.readValue(json, AccessTokenResponse.class);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOException(json, e);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
is.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static AccessTokenResponse invokeRefresh(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure {
|
public static AccessTokenResponse invokeRefresh(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure {
|
||||||
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
|
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
|
||||||
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
|
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
|
||||||
|
|||||||
+49
-62
@@ -17,28 +17,25 @@
|
|||||||
*/
|
*/
|
||||||
package org.keycloak.adapters.authorization;
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.AuthorizationContext;
|
import org.keycloak.AuthorizationContext;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
import org.keycloak.adapters.OIDCHttpFacade;
|
import org.keycloak.adapters.OIDCHttpFacade;
|
||||||
import org.keycloak.adapters.spi.HttpFacade.Request;
|
import org.keycloak.adapters.spi.HttpFacade.Request;
|
||||||
import org.keycloak.adapters.spi.HttpFacade.Response;
|
|
||||||
import org.keycloak.authorization.client.AuthzClient;
|
import org.keycloak.authorization.client.AuthzClient;
|
||||||
import org.keycloak.authorization.client.representation.ResourceRepresentation;
|
import org.keycloak.authorization.client.ClientAuthorizationContext;
|
||||||
import org.keycloak.authorization.client.resource.ProtectedResource;
|
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||||
import org.keycloak.representations.idm.authorization.Permission;
|
import org.keycloak.representations.idm.authorization.Permission;
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
@@ -48,7 +45,7 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
private final PolicyEnforcerConfig enforcerConfig;
|
private final PolicyEnforcerConfig enforcerConfig;
|
||||||
private final PolicyEnforcer policyEnforcer;
|
private final PolicyEnforcer policyEnforcer;
|
||||||
|
|
||||||
private List<PathConfig> paths;
|
private Map<String, PathConfig> paths;
|
||||||
private AuthzClient authzClient;
|
private AuthzClient authzClient;
|
||||||
private PathMatcher pathMatcher;
|
private PathMatcher pathMatcher;
|
||||||
|
|
||||||
@@ -56,8 +53,8 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
this.policyEnforcer = policyEnforcer;
|
this.policyEnforcer = policyEnforcer;
|
||||||
this.enforcerConfig = policyEnforcer.getEnforcerConfig();
|
this.enforcerConfig = policyEnforcer.getEnforcerConfig();
|
||||||
this.authzClient = policyEnforcer.getClient();
|
this.authzClient = policyEnforcer.getClient();
|
||||||
this.pathMatcher = new PathMatcher();
|
this.pathMatcher = policyEnforcer.getPathMatcher();
|
||||||
this.paths = new ArrayList<>(policyEnforcer.getPaths());
|
this.paths = policyEnforcer.getPaths();
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthorizationContext authorize(OIDCHttpFacade httpFacade) {
|
public AuthorizationContext authorize(OIDCHttpFacade httpFacade) {
|
||||||
@@ -74,20 +71,23 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
|
|
||||||
if (accessToken != null) {
|
if (accessToken != null) {
|
||||||
Request request = httpFacade.getRequest();
|
Request request = httpFacade.getRequest();
|
||||||
Response response = httpFacade.getResponse();
|
String path = getPath(request);
|
||||||
String pathInfo = URI.create(request.getURI()).getPath().substring(1);
|
|
||||||
String path = pathInfo.substring(pathInfo.indexOf('/'), pathInfo.length());
|
|
||||||
PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
|
PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
|
||||||
|
|
||||||
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
|
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
|
||||||
|
|
||||||
if (pathConfig == null) {
|
if (pathConfig == null) {
|
||||||
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
|
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
|
||||||
return createAuthorizationContext(accessToken);
|
return createAuthorizationContext(accessToken, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.debugf("Could not find a configuration for path [%s]", path);
|
LOGGER.debugf("Could not find a configuration for path [%s]", path);
|
||||||
response.sendError(403, "Could not find a configuration for path [" + path + "].");
|
|
||||||
|
if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
|
||||||
|
return createAuthorizationContext(accessToken, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAccessDenied(httpFacade);
|
||||||
|
|
||||||
return createEmptyAuthorizationContext(false);
|
return createEmptyAuthorizationContext(false);
|
||||||
}
|
}
|
||||||
@@ -96,20 +96,21 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
return createEmptyAuthorizationContext(true);
|
return createEmptyAuthorizationContext(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
PathConfig actualPathConfig = resolvePathConfig(pathConfig, request);
|
Set<String> requiredScopes = getRequiredScopes(pathConfig, request);
|
||||||
Set<String> requiredScopes = getRequiredScopes(actualPathConfig, request);
|
|
||||||
|
|
||||||
if (isAuthorized(actualPathConfig, requiredScopes, accessToken, httpFacade)) {
|
if (isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
|
||||||
try {
|
try {
|
||||||
return createAuthorizationContext(accessToken);
|
return createAuthorizationContext(accessToken, pathConfig);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Error processing path [" + actualPathConfig.getPath() + "].", e);
|
throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!challenge(actualPathConfig, requiredScopes, httpFacade)) {
|
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
|
||||||
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
|
|
||||||
response.sendError(403, "Authorization failed.");
|
if (!challenge(pathConfig, requiredScopes, httpFacade)) {
|
||||||
|
LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
|
||||||
|
handleAccessDenied(httpFacade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,12 +123,9 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
protected boolean isAuthorized(PathConfig actualPathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) {
|
protected boolean isAuthorized(PathConfig actualPathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) {
|
||||||
Request request = httpFacade.getRequest();
|
Request request = httpFacade.getRequest();
|
||||||
PolicyEnforcerConfig enforcerConfig = getEnforcerConfig();
|
PolicyEnforcerConfig enforcerConfig = getEnforcerConfig();
|
||||||
String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
|
|
||||||
|
|
||||||
if (accessDeniedPath != null) {
|
if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
|
||||||
if (request.getURI().contains(accessDeniedPath)) {
|
return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AccessToken.Authorization authorization = accessToken.getAuthorization();
|
AccessToken.Authorization authorization = accessToken.getAuthorization();
|
||||||
@@ -173,6 +171,21 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void handleAccessDenied(OIDCHttpFacade httpFacade) {
|
||||||
|
httpFacade.getResponse().sendError(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDefaultAccessDeniedUri(Request request, PolicyEnforcerConfig enforcerConfig) {
|
||||||
|
String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
|
||||||
|
|
||||||
|
if (accessDeniedPath != null) {
|
||||||
|
if (request.getURI().contains(accessDeniedPath)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean hasResourceScopePermission(Set<String> requiredScopes, Permission permission, PathConfig actualPathConfig) {
|
private boolean hasResourceScopePermission(Set<String> requiredScopes, Permission permission, PathConfig actualPathConfig) {
|
||||||
Set<String> allowedScopes = permission.getScopes();
|
Set<String> allowedScopes = permission.getScopes();
|
||||||
return (allowedScopes.containsAll(requiredScopes) || allowedScopes.isEmpty());
|
return (allowedScopes.containsAll(requiredScopes) || allowedScopes.isEmpty());
|
||||||
@@ -191,7 +204,7 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) {
|
private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) {
|
||||||
return new AuthorizationContext() {
|
return new ClientAuthorizationContext(authzClient) {
|
||||||
@Override
|
@Override
|
||||||
public boolean hasPermission(String resourceName, String scopeName) {
|
public boolean hasPermission(String resourceName, String scopeName) {
|
||||||
return granted;
|
return granted;
|
||||||
@@ -219,34 +232,8 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private PathConfig resolvePathConfig(PathConfig originalConfig, Request request) {
|
private String getPath(Request request) {
|
||||||
if (originalConfig.hasPattern()) {
|
return request.getRelativePath();
|
||||||
String pathInfo = URI.create(request.getURI()).getPath().substring(1);
|
|
||||||
String path = pathInfo.substring(pathInfo.indexOf('/'), pathInfo.length());
|
|
||||||
ProtectedResource resource = this.authzClient.protection().resource();
|
|
||||||
Set<String> search = resource.findByFilter("uri=" + path);
|
|
||||||
|
|
||||||
if (!search.isEmpty()) {
|
|
||||||
// resource does exist on the server, cache it
|
|
||||||
ResourceRepresentation targetResource = resource.findById(search.iterator().next()).getResourceDescription();
|
|
||||||
PathConfig config = new PathConfig();
|
|
||||||
|
|
||||||
config.setId(targetResource.getId());
|
|
||||||
config.setName(targetResource.getName());
|
|
||||||
config.setType(targetResource.getType());
|
|
||||||
config.setPath(targetResource.getUri());
|
|
||||||
config.setScopes(originalConfig.getScopes());
|
|
||||||
config.setMethods(originalConfig.getMethods());
|
|
||||||
config.setParentConfig(originalConfig);
|
|
||||||
config.setEnforcementMode(originalConfig.getEnforcementMode());
|
|
||||||
|
|
||||||
this.paths.add(config);
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<String> getRequiredScopes(PathConfig pathConfig, Request request) {
|
private Set<String> getRequiredScopes(PathConfig pathConfig, Request request) {
|
||||||
@@ -265,8 +252,8 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
return requiredScopes;
|
return requiredScopes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthorizationContext createAuthorizationContext(AccessToken accessToken) {
|
private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {
|
||||||
return new AuthorizationContext(accessToken, this.paths);
|
return new ClientAuthorizationContext(accessToken, pathConfig, this.paths, authzClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
|
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
|
||||||
|
|||||||
+10
-4
@@ -17,6 +17,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.keycloak.adapters.authorization;
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.adapters.OIDCHttpFacade;
|
import org.keycloak.adapters.OIDCHttpFacade;
|
||||||
import org.keycloak.adapters.spi.HttpFacade;
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
@@ -26,8 +28,6 @@ import org.keycloak.authorization.client.resource.PermissionResource;
|
|||||||
import org.keycloak.authorization.client.resource.ProtectionResource;
|
import org.keycloak.authorization.client.resource.ProtectionResource;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
@@ -52,20 +52,26 @@ public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||||||
private void challengeEntitlementAuthentication(OIDCHttpFacade facade) {
|
private void challengeEntitlementAuthentication(OIDCHttpFacade facade) {
|
||||||
HttpFacade.Response response = facade.getResponse();
|
HttpFacade.Response response = facade.getResponse();
|
||||||
AuthzClient authzClient = getAuthzClient();
|
AuthzClient authzClient = getAuthzClient();
|
||||||
String clientId = authzClient.getConfiguration().getClientId();
|
String clientId = authzClient.getConfiguration().getResource();
|
||||||
String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/entitlement";
|
String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/entitlement";
|
||||||
response.setStatus(401);
|
response.setStatus(401);
|
||||||
response.setHeader("WWW-Authenticate", "KC_ETT realm=\"" + clientId + "\",as_uri=\"" + authorizationServerUri + "\"");
|
response.setHeader("WWW-Authenticate", "KC_ETT realm=\"" + clientId + "\",as_uri=\"" + authorizationServerUri + "\"");
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debug("Sending Entitlement challenge");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void challengeUmaAuthentication(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) {
|
private void challengeUmaAuthentication(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) {
|
||||||
HttpFacade.Response response = facade.getResponse();
|
HttpFacade.Response response = facade.getResponse();
|
||||||
AuthzClient authzClient = getAuthzClient();
|
AuthzClient authzClient = getAuthzClient();
|
||||||
String ticket = getPermissionTicket(pathConfig, requiredScopes, authzClient);
|
String ticket = getPermissionTicket(pathConfig, requiredScopes, authzClient);
|
||||||
String clientId = authzClient.getConfiguration().getClientId();
|
String clientId = authzClient.getConfiguration().getResource();
|
||||||
String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/authorize";
|
String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/authorize";
|
||||||
response.setStatus(401);
|
response.setStatus(401);
|
||||||
response.setHeader("WWW-Authenticate", "UMA realm=\"" + clientId + "\",as_uri=\"" + authorizationServerUri + "\",ticket=\"" + ticket + "\"");
|
response.setHeader("WWW-Authenticate", "UMA realm=\"" + clientId + "\",as_uri=\"" + authorizationServerUri + "\",ticket=\"" + ticket + "\"");
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debug("Sending UMA challenge");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getPermissionTicket(PathConfig pathConfig, Set<String> requiredScopes, AuthzClient authzClient) {
|
private String getPermissionTicket(PathConfig pathConfig, Set<String> requiredScopes, AuthzClient authzClient) {
|
||||||
|
|||||||
+39
-38
@@ -17,6 +17,10 @@
|
|||||||
*/
|
*/
|
||||||
package org.keycloak.adapters.authorization;
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.OIDCHttpFacade;
|
import org.keycloak.adapters.OIDCHttpFacade;
|
||||||
@@ -34,10 +38,6 @@ import org.keycloak.representations.AccessToken;
|
|||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||||
import org.keycloak.representations.idm.authorization.Permission;
|
import org.keycloak.representations.idm.authorization.Permission;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
@@ -51,43 +51,44 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean isAuthorized(PathConfig pathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) {
|
protected boolean isAuthorized(PathConfig pathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) {
|
||||||
int retry = 2;
|
|
||||||
AccessToken original = accessToken;
|
AccessToken original = accessToken;
|
||||||
|
|
||||||
while (retry > 0) {
|
if (super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
|
||||||
if (super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
|
return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
accessToken = requestAuthorizationToken(pathConfig, requiredScopes, httpFacade);
|
|
||||||
|
|
||||||
if (accessToken == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
AccessToken.Authorization authorization = original.getAuthorization();
|
|
||||||
|
|
||||||
if (authorization == null) {
|
|
||||||
authorization = new AccessToken.Authorization();
|
|
||||||
authorization.setPermissions(new ArrayList<Permission>());
|
|
||||||
}
|
|
||||||
|
|
||||||
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
|
|
||||||
|
|
||||||
if (newAuthorization != null) {
|
|
||||||
authorization.getPermissions().addAll(newAuthorization.getPermissions());
|
|
||||||
}
|
|
||||||
|
|
||||||
original.setAuthorization(authorization);
|
|
||||||
|
|
||||||
retry--;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
accessToken = requestAuthorizationToken(pathConfig, requiredScopes, httpFacade);
|
||||||
|
|
||||||
|
if (accessToken == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessToken.Authorization authorization = original.getAuthorization();
|
||||||
|
|
||||||
|
if (authorization == null) {
|
||||||
|
authorization = new AccessToken.Authorization();
|
||||||
|
authorization.setPermissions(new ArrayList<Permission>());
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
|
||||||
|
|
||||||
|
if (newAuthorization != null) {
|
||||||
|
authorization.getPermissions().addAll(newAuthorization.getPermissions());
|
||||||
|
}
|
||||||
|
|
||||||
|
original.setAuthorization(authorization);
|
||||||
|
|
||||||
|
return super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean challenge(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) {
|
protected boolean challenge(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) {
|
||||||
|
handleAccessDenied(facade);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void handleAccessDenied(OIDCHttpFacade facade) {
|
||||||
String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo();
|
String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo();
|
||||||
HttpFacade.Response response = facade.getResponse();
|
HttpFacade.Response response = facade.getResponse();
|
||||||
|
|
||||||
@@ -97,8 +98,6 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||||||
} else {
|
} else {
|
||||||
response.sendError(403);
|
response.sendError(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private AccessToken requestAuthorizationToken(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade httpFacade) {
|
private AccessToken requestAuthorizationToken(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade httpFacade) {
|
||||||
@@ -108,7 +107,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||||||
KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
|
KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
|
||||||
|
|
||||||
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
||||||
LOGGER.debug("Obtaining authorization for authenticated user.");
|
LOGGER.debug("Obtaining authorization for authenticated user.");
|
||||||
PermissionRequest permissionRequest = new PermissionRequest();
|
PermissionRequest permissionRequest = new PermissionRequest();
|
||||||
|
|
||||||
permissionRequest.setResourceSetId(pathConfig.getId());
|
permissionRequest.setResourceSetId(pathConfig.getId());
|
||||||
@@ -128,7 +127,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||||||
AccessToken token = httpFacade.getSecurityContext().getToken();
|
AccessToken token = httpFacade.getSecurityContext().getToken();
|
||||||
|
|
||||||
if (token.getAuthorization() == null) {
|
if (token.getAuthorization() == null) {
|
||||||
EntitlementResponse authzResponse = authzClient.entitlement(accessToken).getAll(authzClient.getConfiguration().getClientId());
|
EntitlementResponse authzResponse = authzClient.entitlement(accessToken).getAll(authzClient.getConfiguration().getResource());
|
||||||
return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
|
return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
|
||||||
} else {
|
} else {
|
||||||
EntitlementRequest request = new EntitlementRequest();
|
EntitlementRequest request = new EntitlementRequest();
|
||||||
@@ -136,12 +135,14 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||||||
permissionRequest.setResourceSetId(pathConfig.getId());
|
permissionRequest.setResourceSetId(pathConfig.getId());
|
||||||
permissionRequest.setResourceSetName(pathConfig.getName());
|
permissionRequest.setResourceSetName(pathConfig.getName());
|
||||||
permissionRequest.setScopes(new HashSet<>(pathConfig.getScopes()));
|
permissionRequest.setScopes(new HashSet<>(pathConfig.getScopes()));
|
||||||
|
LOGGER.debugf("Sending entitlements request: resource_set_id [%s], resource_set_name [%s], scopes [%s].", permissionRequest.getResourceSetId(), permissionRequest.getResourceSetName(), permissionRequest.getScopes());
|
||||||
request.addPermission(permissionRequest);
|
request.addPermission(permissionRequest);
|
||||||
EntitlementResponse authzResponse = authzClient.entitlement(accessToken).get(authzClient.getConfiguration().getClientId(), request);
|
EntitlementResponse authzResponse = authzClient.entitlement(accessToken).get(authzClient.getConfiguration().getResource(), request);
|
||||||
return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
|
return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (AuthorizationDeniedException e) {
|
} catch (AuthorizationDeniedException e) {
|
||||||
|
LOGGER.debug("Authorization denied", e);
|
||||||
return null;
|
return null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Unexpected error during authorization request.", e);
|
throw new RuntimeException("Unexpected error during authorization request.", e);
|
||||||
|
|||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.locks.LockSupport;
|
||||||
|
|
||||||
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class PathCache {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The load factor.
|
||||||
|
*/
|
||||||
|
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
|
||||||
|
|
||||||
|
private final Map<String, CacheEntry> cache;
|
||||||
|
|
||||||
|
private final AtomicBoolean writing = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
private final long maxAge;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*
|
||||||
|
* @param maxEntries the maximum number of entries to keep in the cache
|
||||||
|
*/
|
||||||
|
public PathCache(int maxEntries) {
|
||||||
|
this(maxEntries, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*
|
||||||
|
* @param maxEntries the maximum number of entries to keep in the cache
|
||||||
|
* @param maxAge the time in milliseconds that an entry can stay in the cache. If {@code -1}, entries never expire
|
||||||
|
*/
|
||||||
|
public PathCache(final int maxEntries, long maxAge) {
|
||||||
|
cache = new LinkedHashMap<String, CacheEntry>(16, DEFAULT_LOAD_FACTOR, true) {
|
||||||
|
@Override
|
||||||
|
protected boolean removeEldestEntry(Map.Entry eldest) {
|
||||||
|
return cache.size() > maxEntries;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.maxAge = maxAge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void put(String uri, PathConfig newValue) {
|
||||||
|
try {
|
||||||
|
if (parkForWriteAndCheckInterrupt()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheEntry cacheEntry = cache.get(uri);
|
||||||
|
|
||||||
|
if (cacheEntry == null) {
|
||||||
|
cache.put(uri, new CacheEntry(uri, newValue, maxAge));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
writing.lazySet(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PathConfig get(String uri) {
|
||||||
|
if (parkForReadAndCheckInterrupt()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheEntry cached = cache.get(uri);
|
||||||
|
|
||||||
|
if (cached != null) {
|
||||||
|
return removeIfExpired(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(String key) {
|
||||||
|
try {
|
||||||
|
if (parkForWriteAndCheckInterrupt()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.remove(key);
|
||||||
|
} finally {
|
||||||
|
writing.lazySet(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PathConfig removeIfExpired(CacheEntry cached) {
|
||||||
|
if (cached == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cached.isExpired()) {
|
||||||
|
remove(cached.key());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean parkForWriteAndCheckInterrupt() {
|
||||||
|
while (!writing.compareAndSet(false, true)) {
|
||||||
|
LockSupport.parkNanos(1L);
|
||||||
|
if (Thread.interrupted()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean parkForReadAndCheckInterrupt() {
|
||||||
|
while (writing.get()) {
|
||||||
|
LockSupport.parkNanos(1L);
|
||||||
|
if (Thread.interrupted()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CacheEntry {
|
||||||
|
|
||||||
|
final String key;
|
||||||
|
final PathConfig value;
|
||||||
|
final long expiration;
|
||||||
|
|
||||||
|
CacheEntry(String key, PathConfig value, long maxAge) {
|
||||||
|
this.key = key;
|
||||||
|
this.value = value;
|
||||||
|
if(maxAge == -1) {
|
||||||
|
expiration = -1;
|
||||||
|
} else {
|
||||||
|
expiration = System.currentTimeMillis() + maxAge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String key() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
PathConfig value() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isExpired() {
|
||||||
|
return expiration != -1 ? System.currentTimeMillis() > expiration : false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+199
-60
@@ -17,87 +17,226 @@
|
|||||||
*/
|
*/
|
||||||
package org.keycloak.adapters.authorization;
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import java.util.List;
|
import org.keycloak.authorization.client.AuthzClient;
|
||||||
|
import org.keycloak.authorization.client.representation.ResourceRepresentation;
|
||||||
|
import org.keycloak.authorization.client.resource.ProtectedResource;
|
||||||
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
class PathMatcher {
|
class PathMatcher {
|
||||||
|
|
||||||
private static final String ANY_RESOURCE_PATTERN = "/*";
|
private static final char WILDCARD = '*';
|
||||||
|
private final AuthzClient authzClient;
|
||||||
|
// TODO: make this configurable
|
||||||
|
private PathCache cache = new PathCache(100, 30000);
|
||||||
|
|
||||||
PathConfig matches(final String requestedUri, List<PathConfig> paths) {
|
public PathMatcher(AuthzClient authzClient) {
|
||||||
PathConfig actualConfig = null;
|
this.authzClient = authzClient;
|
||||||
|
}
|
||||||
|
|
||||||
for (PathConfig entry : paths) {
|
public PathConfig matches(final String targetUri, Map<String, PathConfig> paths) {
|
||||||
String protectedUri = entry.getPath();
|
PathConfig pathConfig = paths.get(targetUri) == null ? cache.get(targetUri) : paths.get(targetUri);
|
||||||
String selectedUri = null;
|
|
||||||
|
|
||||||
if (protectedUri.equals(ANY_RESOURCE_PATTERN) && actualConfig == null) {
|
if (pathConfig != null) {
|
||||||
selectedUri = protectedUri;
|
return pathConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
PathConfig matchingAnyPath = null;
|
||||||
|
PathConfig matchingAnySuffixPath = null;
|
||||||
|
|
||||||
|
for (PathConfig entry : paths.values()) {
|
||||||
|
String expectedUri = entry.getPath();
|
||||||
|
String matchingUri = null;
|
||||||
|
|
||||||
|
if (exactMatch(expectedUri, targetUri, expectedUri)) {
|
||||||
|
matchingUri = expectedUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
int suffixIndex = protectedUri.indexOf(ANY_RESOURCE_PATTERN + ".");
|
if (isTemplate(expectedUri)) {
|
||||||
|
String templateUri = buildUriFromTemplate(expectedUri, targetUri);
|
||||||
|
|
||||||
if (suffixIndex != -1) {
|
if (templateUri != null) {
|
||||||
String protectedSuffix = protectedUri.substring(suffixIndex + ANY_RESOURCE_PATTERN.length());
|
if (exactMatch(expectedUri, targetUri, templateUri)) {
|
||||||
|
matchingUri = templateUri;
|
||||||
if (requestedUri.endsWith(protectedSuffix)) {
|
entry = resolvePathConfig(entry, targetUri);
|
||||||
selectedUri = protectedUri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (protectedUri.equals(requestedUri)) {
|
|
||||||
selectedUri = protectedUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (protectedUri.endsWith(ANY_RESOURCE_PATTERN)) {
|
|
||||||
String formattedPattern = removeWildCardsFromUri(protectedUri);
|
|
||||||
|
|
||||||
if (!formattedPattern.equals("/") && requestedUri.startsWith(formattedPattern)) {
|
|
||||||
selectedUri = protectedUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formattedPattern.equals("/") && formattedPattern.endsWith("/") && formattedPattern.substring(0, formattedPattern.length() - 1).equals(requestedUri)) {
|
|
||||||
selectedUri = protectedUri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int startRegex = protectedUri.indexOf('{');
|
|
||||||
|
|
||||||
if (startRegex != -1) {
|
|
||||||
String prefix = protectedUri.substring(0, startRegex);
|
|
||||||
|
|
||||||
if (requestedUri.startsWith(prefix)) {
|
|
||||||
selectedUri = protectedUri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedUri != null) {
|
|
||||||
selectedUri = protectedUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedUri != null) {
|
|
||||||
if (actualConfig == null) {
|
|
||||||
actualConfig = entry;
|
|
||||||
} else {
|
|
||||||
if (actualConfig.equals(ANY_RESOURCE_PATTERN)) {
|
|
||||||
actualConfig = entry;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (protectedUri.startsWith(removeWildCardsFromUri(actualConfig.getPath()))) {
|
if (matchingUri != null) {
|
||||||
actualConfig = entry;
|
StringBuilder path = new StringBuilder(expectedUri);
|
||||||
|
int patternIndex = path.indexOf("/" + WILDCARD);
|
||||||
|
|
||||||
|
if (patternIndex != -1) {
|
||||||
|
path.delete(patternIndex, path.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
patternIndex = path.indexOf("{");
|
||||||
|
|
||||||
|
if (patternIndex != -1) {
|
||||||
|
path.delete(patternIndex, path.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
String pathString = path.toString();
|
||||||
|
|
||||||
|
if ("".equals(pathString)) {
|
||||||
|
pathString = "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingUri.equals(targetUri)) {
|
||||||
|
cache.put(targetUri, entry);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WILDCARD == expectedUri.charAt(expectedUri.length() - 1)) {
|
||||||
|
matchingAnyPath = entry;
|
||||||
|
} else {
|
||||||
|
int suffixIndex = expectedUri.indexOf(WILDCARD + ".");
|
||||||
|
|
||||||
|
if (suffixIndex != -1) {
|
||||||
|
String protectedSuffix = expectedUri.substring(suffixIndex + 1);
|
||||||
|
|
||||||
|
if (targetUri.endsWith(protectedSuffix)) {
|
||||||
|
matchingAnySuffixPath = entry;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return actualConfig;
|
if (matchingAnySuffixPath != null) {
|
||||||
|
cache.put(targetUri, matchingAnySuffixPath);
|
||||||
|
return matchingAnySuffixPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingAnyPath != null) {
|
||||||
|
cache.put(targetUri, matchingAnyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchingAnyPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String removeWildCardsFromUri(String protectedUri) {
|
private boolean exactMatch(String expectedUri, String targetUri, String value) {
|
||||||
return protectedUri.replaceAll("/[*]", "/");
|
if (targetUri.equals(value)) {
|
||||||
|
return value.equals(targetUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endsWithWildcard(expectedUri)) {
|
||||||
|
return targetUri.startsWith(expectedUri.substring(0, expectedUri.length() - 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
String suffix = "/*.";
|
||||||
|
int suffixIndex = expectedUri.indexOf(suffix);
|
||||||
|
|
||||||
|
if (suffixIndex != -1) {
|
||||||
|
return targetUri.endsWith(expectedUri.substring(suffixIndex + suffix.length() - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String buildUriFromTemplate(String expectedUri, String targetUri) {
|
||||||
|
int patternStartIndex = expectedUri.indexOf("{");
|
||||||
|
|
||||||
|
if (patternStartIndex == -1 || patternStartIndex >= targetUri.length()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedUri.split("/").length > targetUri.split("/").length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
char[] expectedUriChars = expectedUri.toCharArray();
|
||||||
|
char[] matchingUri = Arrays.copyOfRange(expectedUriChars, 0, patternStartIndex);
|
||||||
|
int matchingUriLastIndex = matchingUri.length;
|
||||||
|
String targetUriParams = targetUri.substring(patternStartIndex);
|
||||||
|
|
||||||
|
if (Arrays.equals(matchingUri, Arrays.copyOf(targetUri.toCharArray(), matchingUri.length))) {
|
||||||
|
matchingUri = Arrays.copyOf(matchingUri, targetUri.length());
|
||||||
|
int paramIndex = 0;
|
||||||
|
|
||||||
|
for (int i = patternStartIndex; i < expectedUriChars.length; i++) {
|
||||||
|
if (matchingUriLastIndex >= matchingUri.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
char c = expectedUriChars[i];
|
||||||
|
|
||||||
|
if (c == '{' || c == '*') {
|
||||||
|
String[] params = targetUriParams.split("/");
|
||||||
|
|
||||||
|
for (int k = paramIndex; k <= (c == '*' ? params.length : paramIndex); k++) {
|
||||||
|
if (k == params.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int paramLength = params[k].length();
|
||||||
|
|
||||||
|
if (matchingUriLastIndex + paramLength > matchingUri.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int j = 0; j < paramLength; j++) {
|
||||||
|
matchingUri[matchingUriLastIndex++] = params[k].charAt(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '*' && matchingUriLastIndex < matchingUri.length) {
|
||||||
|
matchingUri[matchingUriLastIndex++] = '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i = expectedUri.indexOf('}', i);
|
||||||
|
} else {
|
||||||
|
if (c == '/') {
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
matchingUri[matchingUriLastIndex++] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingUri[matchingUri.length - 1] == '\u0000') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.valueOf(matchingUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean endsWithWildcard(String expectedUri) {
|
||||||
|
return WILDCARD == expectedUri.charAt(expectedUri.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTemplate(String uri) {
|
||||||
|
return uri.indexOf("{") != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PathConfig resolvePathConfig(PathConfig originalConfig, String path) {
|
||||||
|
if (originalConfig.hasPattern()) {
|
||||||
|
ProtectedResource resource = this.authzClient.protection().resource();
|
||||||
|
Set<String> search = resource.findByFilter("uri=" + path);
|
||||||
|
|
||||||
|
if (!search.isEmpty()) {
|
||||||
|
// resource does exist on the server, cache it
|
||||||
|
ResourceRepresentation targetResource = resource.findById(search.iterator().next()).getResourceDescription();
|
||||||
|
PathConfig config = PolicyEnforcer.createPathConfig(targetResource);
|
||||||
|
|
||||||
|
config.setScopes(originalConfig.getScopes());
|
||||||
|
config.setMethods(originalConfig.getMethods());
|
||||||
|
config.setParentConfig(originalConfig);
|
||||||
|
config.setEnforcementMode(originalConfig.getEnforcementMode());
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+42
-15
@@ -21,7 +21,9 @@ import org.jboss.logging.Logger;
|
|||||||
import org.keycloak.AuthorizationContext;
|
import org.keycloak.AuthorizationContext;
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.OIDCHttpFacade;
|
import org.keycloak.adapters.OIDCHttpFacade;
|
||||||
|
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
|
||||||
import org.keycloak.authorization.client.AuthzClient;
|
import org.keycloak.authorization.client.AuthzClient;
|
||||||
|
import org.keycloak.authorization.client.ClientAuthenticator;
|
||||||
import org.keycloak.authorization.client.Configuration;
|
import org.keycloak.authorization.client.Configuration;
|
||||||
import org.keycloak.authorization.client.representation.RegistrationResponse;
|
import org.keycloak.authorization.client.representation.RegistrationResponse;
|
||||||
import org.keycloak.authorization.client.representation.ResourceRepresentation;
|
import org.keycloak.authorization.client.representation.ResourceRepresentation;
|
||||||
@@ -34,8 +36,10 @@ import org.keycloak.representations.idm.authorization.Permission;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,17 +52,24 @@ public class PolicyEnforcer {
|
|||||||
private final KeycloakDeployment deployment;
|
private final KeycloakDeployment deployment;
|
||||||
private final AuthzClient authzClient;
|
private final AuthzClient authzClient;
|
||||||
private final PolicyEnforcerConfig enforcerConfig;
|
private final PolicyEnforcerConfig enforcerConfig;
|
||||||
private final List<PathConfig> paths;
|
private final Map<String, PathConfig> paths;
|
||||||
|
private final PathMatcher pathMatcher;
|
||||||
|
|
||||||
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
|
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
|
||||||
this.deployment = deployment;
|
this.deployment = deployment;
|
||||||
this.enforcerConfig = adapterConfig.getPolicyEnforcerConfig();
|
this.enforcerConfig = adapterConfig.getPolicyEnforcerConfig();
|
||||||
this.authzClient = AuthzClient.create(new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), deployment.getClient()));
|
this.authzClient = AuthzClient.create(new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), deployment.getClient()), new ClientAuthenticator() {
|
||||||
|
@Override
|
||||||
|
public void configureClientCredentials(HashMap<String, String> requestParams, HashMap<String, String> requestHeaders) {
|
||||||
|
ClientCredentialsProviderUtils.setClientCredentials(PolicyEnforcer.this.deployment, requestHeaders, requestParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.pathMatcher = new PathMatcher(this.authzClient);
|
||||||
this.paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
|
this.paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debug("Initialization complete. Path configurations:");
|
LOGGER.debug("Initialization complete. Path configurations:");
|
||||||
for (PathConfig pathConfig : this.paths) {
|
for (PathConfig pathConfig : this.paths.values()) {
|
||||||
LOGGER.debug(pathConfig);
|
LOGGER.debug(pathConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,15 +107,19 @@ public class PolicyEnforcer {
|
|||||||
return authzClient;
|
return authzClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<PathConfig> getPaths() {
|
public Map<String, PathConfig> getPaths() {
|
||||||
return Collections.unmodifiableList(paths);
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addPath(PathConfig pathConfig) {
|
||||||
|
paths.put(pathConfig.getPath(), pathConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
KeycloakDeployment getDeployment() {
|
KeycloakDeployment getDeployment() {
|
||||||
return deployment;
|
return deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
private Map<String, PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
||||||
boolean loadPathsFromServer = true;
|
boolean loadPathsFromServer = true;
|
||||||
|
|
||||||
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
|
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
|
||||||
@@ -123,8 +138,8 @@ public class PolicyEnforcer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
private Map<String, PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
||||||
List<PathConfig> paths = new ArrayList<>();
|
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
|
||||||
|
|
||||||
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
|
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
|
||||||
Set<String> search;
|
Set<String> search;
|
||||||
@@ -172,7 +187,7 @@ public class PolicyEnforcer {
|
|||||||
|
|
||||||
PathConfig existingPath = null;
|
PathConfig existingPath = null;
|
||||||
|
|
||||||
for (PathConfig current : paths) {
|
for (PathConfig current : paths.values()) {
|
||||||
if (current.getId().equals(pathConfig.getId()) && current.getPath().equals(pathConfig.getPath())) {
|
if (current.getId().equals(pathConfig.getId()) && current.getPath().equals(pathConfig.getPath())) {
|
||||||
existingPath = current;
|
existingPath = current;
|
||||||
break;
|
break;
|
||||||
@@ -180,7 +195,7 @@ public class PolicyEnforcer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (existingPath == null) {
|
if (existingPath == null) {
|
||||||
paths.add(pathConfig);
|
paths.put(pathConfig.getPath(), pathConfig);
|
||||||
} else {
|
} else {
|
||||||
existingPath.getMethods().addAll(pathConfig.getMethods());
|
existingPath.getMethods().addAll(pathConfig.getMethods());
|
||||||
existingPath.getScopes().addAll(pathConfig.getScopes());
|
existingPath.getScopes().addAll(pathConfig.getScopes());
|
||||||
@@ -190,28 +205,36 @@ public class PolicyEnforcer {
|
|||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<PathConfig> configureAllPathsForResourceServer(ProtectedResource protectedResource) {
|
private Map<String, PathConfig> configureAllPathsForResourceServer(ProtectedResource protectedResource) {
|
||||||
LOGGER.info("Querying the server for all resources associated with this application.");
|
LOGGER.info("Querying the server for all resources associated with this application.");
|
||||||
List<PathConfig> paths = new ArrayList<>();
|
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
|
||||||
|
|
||||||
for (String id : protectedResource.findAll()) {
|
for (String id : protectedResource.findAll()) {
|
||||||
RegistrationResponse response = protectedResource.findById(id);
|
RegistrationResponse response = protectedResource.findById(id);
|
||||||
ResourceRepresentation resourceDescription = response.getResourceDescription();
|
ResourceRepresentation resourceDescription = response.getResourceDescription();
|
||||||
|
|
||||||
if (resourceDescription.getUri() != null) {
|
if (resourceDescription.getUri() != null) {
|
||||||
paths.add(createPathConfig(resourceDescription));
|
PathConfig pathConfig = createPathConfig(resourceDescription);
|
||||||
|
paths.put(pathConfig.getPath(), pathConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
|
static PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
|
||||||
PathConfig pathConfig = new PathConfig();
|
PathConfig pathConfig = new PathConfig();
|
||||||
|
|
||||||
pathConfig.setId(resourceDescription.getId());
|
pathConfig.setId(resourceDescription.getId());
|
||||||
pathConfig.setName(resourceDescription.getName());
|
pathConfig.setName(resourceDescription.getName());
|
||||||
pathConfig.setPath(resourceDescription.getUri());
|
|
||||||
|
String uri = resourceDescription.getUri();
|
||||||
|
|
||||||
|
if (uri == null || "".equals(uri.trim())) {
|
||||||
|
throw new RuntimeException("Failed to configure paths. Resource [" + resourceDescription.getName() + "] has an invalid or empty URI [" + uri + "].");
|
||||||
|
}
|
||||||
|
|
||||||
|
pathConfig.setPath(uri);
|
||||||
|
|
||||||
List<String> scopeNames = new ArrayList<>();
|
List<String> scopeNames = new ArrayList<>();
|
||||||
|
|
||||||
@@ -224,4 +247,8 @@ public class PolicyEnforcer {
|
|||||||
|
|
||||||
return pathConfig;
|
return pathConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PathMatcher getPathMatcher() {
|
||||||
|
return pathMatcher;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+5
@@ -37,4 +37,9 @@ public class HardcodedPublicKeyLocator implements PublicKeyLocator {
|
|||||||
public PublicKey getPublicKey(String kid, KeycloakDeployment deployment) {
|
public PublicKey getPublicKey(String kid, KeycloakDeployment deployment) {
|
||||||
return publicKey;
|
return publicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset(KeycloakDeployment deployment) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+30
-16
@@ -25,7 +25,6 @@ import org.keycloak.adapters.KeycloakDeployment;
|
|||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||||
import org.keycloak.jose.jwk.JWK;
|
import org.keycloak.jose.jwk.JWK;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
|
||||||
import org.keycloak.util.JWKSUtils;
|
import org.keycloak.util.JWKSUtils;
|
||||||
|
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
@@ -48,36 +47,51 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
|
|||||||
@Override
|
@Override
|
||||||
public PublicKey getPublicKey(String kid, KeycloakDeployment deployment) {
|
public PublicKey getPublicKey(String kid, KeycloakDeployment deployment) {
|
||||||
int minTimeBetweenRequests = deployment.getMinTimeBetweenJwksRequests();
|
int minTimeBetweenRequests = deployment.getMinTimeBetweenJwksRequests();
|
||||||
|
int publicKeyCacheTtl = deployment.getPublicKeyCacheTtl();
|
||||||
|
int currentTime = Time.currentTime();
|
||||||
|
|
||||||
// Check if key is in cache.
|
// Check if key is in cache.
|
||||||
PublicKey publicKey = currentKeys.get(kid);
|
PublicKey publicKey = lookupCachedKey(publicKeyCacheTtl, currentTime, kid);
|
||||||
if (publicKey != null) {
|
if (publicKey != null) {
|
||||||
return publicKey;
|
return publicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
int currentTime = Time.currentTime();
|
|
||||||
|
|
||||||
// Check if we are allowed to send request
|
// Check if we are allowed to send request
|
||||||
if (currentTime > lastRequestTime + minTimeBetweenRequests) {
|
synchronized (this) {
|
||||||
synchronized (this) {
|
currentTime = Time.currentTime();
|
||||||
currentTime = Time.currentTime();
|
if (currentTime > lastRequestTime + minTimeBetweenRequests) {
|
||||||
if (currentTime > lastRequestTime + minTimeBetweenRequests) {
|
sendRequest(deployment);
|
||||||
sendRequest(deployment);
|
lastRequestTime = currentTime;
|
||||||
lastRequestTime = currentTime;
|
} else {
|
||||||
} else {
|
log.debug("Won't send request to realm jwks url. Last request time was " + lastRequestTime);
|
||||||
log.debugf("Won't send request to realm jwks url. Last request time was %d", lastRequestTime);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return lookupCachedKey(publicKeyCacheTtl, currentTime, kid);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return currentKeys.get(kid);
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset(KeycloakDeployment deployment) {
|
||||||
|
synchronized (this) {
|
||||||
|
sendRequest(deployment);
|
||||||
|
lastRequestTime = Time.currentTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private PublicKey lookupCachedKey(int publicKeyCacheTtl, int currentTime, String kid) {
|
||||||
|
if (lastRequestTime + publicKeyCacheTtl > currentTime) {
|
||||||
|
return currentKeys.get(kid);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void sendRequest(KeycloakDeployment deployment) {
|
private void sendRequest(KeycloakDeployment deployment) {
|
||||||
if (log.isTraceEnabled()) {
|
if (log.isTraceEnabled()) {
|
||||||
log.tracef("Going to send request to retrieve new set of realm public keys for client %s", deployment.getResourceName());
|
log.trace("Going to send request to retrieve new set of realm public keys for client " + deployment.getResourceName());
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpGet getMethod = new HttpGet(deployment.getJwksUrl());
|
HttpGet getMethod = new HttpGet(deployment.getJwksUrl());
|
||||||
@@ -87,7 +101,7 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
|
|||||||
Map<String, PublicKey> publicKeys = JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG);
|
Map<String, PublicKey> publicKeys = JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG);
|
||||||
|
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
log.debugf("Realm public keys successfully retrieved for client %s. New kids: %s", deployment.getResourceName(), publicKeys.keySet().toString());
|
log.debug("Realm public keys successfully retrieved for client " + deployment.getResourceName() + ". New kids: " + publicKeys.keySet().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update current keys
|
// Update current keys
|
||||||
|
|||||||
+7
@@ -34,4 +34,11 @@ public interface PublicKeyLocator {
|
|||||||
*/
|
*/
|
||||||
PublicKey getPublicKey(String kid, KeycloakDeployment deployment);
|
PublicKey getPublicKey(String kid, KeycloakDeployment deployment);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the state of locator (eg. clear the cached keys)
|
||||||
|
*
|
||||||
|
* @param deployment
|
||||||
|
*/
|
||||||
|
void reset(KeycloakDeployment deployment);
|
||||||
|
|
||||||
}
|
}
|
||||||
+8
@@ -29,10 +29,13 @@ import org.keycloak.common.util.PemUtils;
|
|||||||
import org.keycloak.enums.TokenStore;
|
import org.keycloak.enums.TokenStore;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
* @author <a href="mailto:brad.culley@spartasystems.com">Brad Culley</a>
|
||||||
|
* @author <a href="mailto:john.ament@spartasystems.com">John D. Ament</a>
|
||||||
*/
|
*/
|
||||||
public class KeycloakDeploymentBuilderTest {
|
public class KeycloakDeploymentBuilderTest {
|
||||||
|
|
||||||
@@ -53,10 +56,12 @@ public class KeycloakDeploymentBuilderTest {
|
|||||||
assertEquals(1000, deployment.getCorsMaxAge());
|
assertEquals(1000, deployment.getCorsMaxAge());
|
||||||
assertEquals("POST, PUT, DELETE, GET", deployment.getCorsAllowedMethods());
|
assertEquals("POST, PUT, DELETE, GET", deployment.getCorsAllowedMethods());
|
||||||
assertEquals("X-Custom, X-Custom2", deployment.getCorsAllowedHeaders());
|
assertEquals("X-Custom, X-Custom2", deployment.getCorsAllowedHeaders());
|
||||||
|
assertEquals("X-Custom3, X-Custom4", deployment.getCorsExposedHeaders());
|
||||||
assertTrue(deployment.isBearerOnly());
|
assertTrue(deployment.isBearerOnly());
|
||||||
assertTrue(deployment.isPublicClient());
|
assertTrue(deployment.isPublicClient());
|
||||||
assertTrue(deployment.isEnableBasicAuth());
|
assertTrue(deployment.isEnableBasicAuth());
|
||||||
assertTrue(deployment.isExposeToken());
|
assertTrue(deployment.isExposeToken());
|
||||||
|
assertFalse(deployment.isOAuthQueryParameterEnabled());
|
||||||
assertEquals("234234-234234-234234", deployment.getResourceCredentials().get("secret"));
|
assertEquals("234234-234234-234234", deployment.getResourceCredentials().get("secret"));
|
||||||
assertEquals(ClientIdAndSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
|
assertEquals(ClientIdAndSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
|
||||||
assertEquals(20, ((ThreadSafeClientConnManager) deployment.getClient().getConnectionManager()).getMaxTotal());
|
assertEquals(20, ((ThreadSafeClientConnManager) deployment.getClient().getConnectionManager()).getMaxTotal());
|
||||||
@@ -69,6 +74,8 @@ public class KeycloakDeploymentBuilderTest {
|
|||||||
assertEquals("email", deployment.getPrincipalAttribute());
|
assertEquals("email", deployment.getPrincipalAttribute());
|
||||||
assertEquals(10, deployment.getTokenMinimumTimeToLive());
|
assertEquals(10, deployment.getTokenMinimumTimeToLive());
|
||||||
assertEquals(20, deployment.getMinTimeBetweenJwksRequests());
|
assertEquals(20, deployment.getMinTimeBetweenJwksRequests());
|
||||||
|
assertEquals(120, deployment.getPublicKeyCacheTtl());
|
||||||
|
assertEquals("/api/$1", deployment.getRedirectRewriteRules().get("^/wsmaster/api/(.*)$"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -78,6 +85,7 @@ public class KeycloakDeploymentBuilderTest {
|
|||||||
|
|
||||||
assertTrue(deployment.getPublicKeyLocator() instanceof JWKPublicKeyLocator);
|
assertTrue(deployment.getPublicKeyLocator() instanceof JWKPublicKeyLocator);
|
||||||
assertEquals(10, deployment.getMinTimeBetweenJwksRequests());
|
assertEquals(10, deployment.getMinTimeBetweenJwksRequests());
|
||||||
|
assertEquals(86400, deployment.getPublicKeyCacheTtl());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:brad.culley@spartasystems.com">Brad Culley</a>
|
||||||
|
* @author <a href="mailto:john.ament@spartasystems.com">John D. Ament</a>
|
||||||
|
*/
|
||||||
|
public class KeycloakDeploymentTest {
|
||||||
|
@Test
|
||||||
|
public void shouldNotEnableOAuthQueryParamWhenIgnoreIsTrue() {
|
||||||
|
KeycloakDeployment keycloakDeployment = new KeycloakDeployment();
|
||||||
|
keycloakDeployment.setIgnoreOAuthQueryParameter(true);
|
||||||
|
assertFalse(keycloakDeployment.isOAuthQueryParameterEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldEnableOAuthQueryParamWhenIgnoreIsFalse() {
|
||||||
|
KeycloakDeployment keycloakDeployment = new KeycloakDeployment();
|
||||||
|
keycloakDeployment.setIgnoreOAuthQueryParameter(false);
|
||||||
|
assertTrue(keycloakDeployment.isOAuthQueryParameterEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldEnableOAuthQueryParamWhenIgnoreNotSet() {
|
||||||
|
KeycloakDeployment keycloakDeployment = new KeycloakDeployment();
|
||||||
|
|
||||||
|
assertTrue(keycloakDeployment.isOAuthQueryParameterEnabled());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"cors-max-age": 1000,
|
"cors-max-age": 1000,
|
||||||
"cors-allowed-methods": "POST, PUT, DELETE, GET",
|
"cors-allowed-methods": "POST, PUT, DELETE, GET",
|
||||||
"cors-allowed-headers": "X-Custom, X-Custom2",
|
"cors-allowed-headers": "X-Custom, X-Custom2",
|
||||||
|
"cors-exposed-headers": "X-Custom3, X-Custom4",
|
||||||
"bearer-only": true,
|
"bearer-only": true,
|
||||||
"public-client": true,
|
"public-client": true,
|
||||||
"enable-basic-auth": true,
|
"enable-basic-auth": true,
|
||||||
@@ -30,5 +31,10 @@
|
|||||||
"token-store": "cookie",
|
"token-store": "cookie",
|
||||||
"principal-attribute": "email",
|
"principal-attribute": "email",
|
||||||
"token-minimum-time-to-live": 10,
|
"token-minimum-time-to-live": 10,
|
||||||
"min-time-between-jwks-requests": 20
|
"min-time-between-jwks-requests": 20,
|
||||||
|
"public-key-cache-ttl": 120,
|
||||||
|
"ignore-oauth-query-parameter": true,
|
||||||
|
"redirect-rewrite-rules" : {
|
||||||
|
"^/wsmaster/api/(.*)$" : "/api/$1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -88,7 +88,6 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jboss.logging</groupId>
|
<groupId>org.jboss.logging</groupId>
|
||||||
<artifactId>jboss-logging</artifactId>
|
<artifactId>jboss-logging</artifactId>
|
||||||
<version>3.1.0.GA</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -104,7 +103,6 @@ projects that depend on this project.-->
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jboss.msc</groupId>
|
<groupId>org.jboss.msc</groupId>
|
||||||
<artifactId>jboss-msc</artifactId>
|
<artifactId>jboss-msc</artifactId>
|
||||||
<version>1.0.2.GA</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<name>Keycloak AS7 / JBoss EAP 6 Integration</name>
|
<name>Keycloak AS7 / JBoss EAP 6 Integration</name>
|
||||||
@@ -30,6 +30,18 @@
|
|||||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.as</groupId>
|
||||||
|
<artifactId>jboss-as-parent</artifactId>
|
||||||
|
<version>${jboss.as.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>as7-adapter-spi</module>
|
<module>as7-adapter-spi</module>
|
||||||
<module>as7-adapter</module>
|
<module>as7-adapter</module>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ public class JaxrsHttpFacade implements OIDCHttpFacade {
|
|||||||
return requestContext.getUriInfo().getRequestUri().toString();
|
return requestContext.getUriInfo().getRequestUri().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRelativePath() {
|
||||||
|
return requestContext.getUriInfo().getPath();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSecure() {
|
public boolean isSecure() {
|
||||||
return securityContext.isSecure();
|
return securityContext.isSecure();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+3
-1
@@ -106,6 +106,8 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica
|
|||||||
|
|
||||||
public abstract AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment);
|
public abstract AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment);
|
||||||
|
|
||||||
|
public abstract JettyUserSessionManagement createSessionManagement(Request request);
|
||||||
|
|
||||||
public void logoutCurrent(Request request) {
|
public void logoutCurrent(Request request) {
|
||||||
AdapterDeploymentContext deploymentContext = (AdapterDeploymentContext) request.getAttribute(AdapterDeploymentContext.class.getName());
|
AdapterDeploymentContext deploymentContext = (AdapterDeploymentContext) request.getAttribute(AdapterDeploymentContext.class.getName());
|
||||||
KeycloakSecurityContext ksc = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
|
KeycloakSecurityContext ksc = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
|
||||||
@@ -287,7 +289,7 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica
|
|||||||
log.debug("*** deployment isn't configured return false");
|
log.debug("*** deployment isn't configured return false");
|
||||||
return Authentication.UNAUTHENTICATED;
|
return Authentication.UNAUTHENTICATED;
|
||||||
}
|
}
|
||||||
PreAuthActionsHandler handler = new PreAuthActionsHandler(new JettyUserSessionManagement(request.getSessionManager()), deploymentContext, facade);
|
PreAuthActionsHandler handler = new PreAuthActionsHandler(createSessionManagement(request), deploymentContext, facade);
|
||||||
if (handler.handleRequest()) {
|
if (handler.handleRequest()) {
|
||||||
return Authentication.SEND_SUCCESS;
|
return Authentication.SEND_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.jetty;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.server.SessionManager;
|
||||||
|
import org.keycloak.adapters.jetty.spi.JettySessionManager;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class Jetty81SessionManager implements JettySessionManager {
|
||||||
|
protected SessionManager sessionManager;
|
||||||
|
|
||||||
|
public Jetty81SessionManager(SessionManager sessionManager) {
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpSession getHttpSession(String id) {
|
||||||
|
return sessionManager.getHttpSession(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -25,6 +25,7 @@ import org.keycloak.adapters.AdapterTokenStore;
|
|||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.jetty.core.AbstractKeycloakJettyAuthenticator;
|
import org.keycloak.adapters.jetty.core.AbstractKeycloakJettyAuthenticator;
|
||||||
import org.keycloak.adapters.jetty.core.JettySessionTokenStore;
|
import org.keycloak.adapters.jetty.core.JettySessionTokenStore;
|
||||||
|
import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement;
|
||||||
|
|
||||||
import javax.servlet.ServletRequest;
|
import javax.servlet.ServletRequest;
|
||||||
|
|
||||||
@@ -44,6 +45,11 @@ public class KeycloakJettyAuthenticator extends AbstractKeycloakJettyAuthenticat
|
|||||||
return new JettySessionTokenStore(request, resolvedDeployment, new JettyAdapterSessionStore(request));
|
return new JettySessionTokenStore(request, resolvedDeployment, new JettyAdapterSessionStore(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JettyUserSessionManagement createSessionManagement(Request request) {
|
||||||
|
return new JettyUserSessionManagement(new Jetty81SessionManager(request.getSessionManager()));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Request resolveRequest(ServletRequest req) {
|
protected Request resolveRequest(ServletRequest req) {
|
||||||
return (req instanceof Request)?(Request)req: AbstractHttpConnection.getCurrentConnection().getRequest();
|
return (req instanceof Request)?(Request)req: AbstractHttpConnection.getCurrentConnection().getRequest();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.jetty;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.server.SessionManager;
|
||||||
|
import org.keycloak.adapters.jetty.spi.JettySessionManager;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class Jetty91SessionManager implements JettySessionManager {
|
||||||
|
protected SessionManager sessionManager;
|
||||||
|
|
||||||
|
public Jetty91SessionManager(SessionManager sessionManager) {
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpSession getHttpSession(String id) {
|
||||||
|
return sessionManager.getHttpSession(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -27,6 +27,7 @@ import org.keycloak.adapters.jetty.core.AbstractKeycloakJettyAuthenticator;
|
|||||||
import org.keycloak.adapters.jetty.core.JettyRequestAuthenticator;
|
import org.keycloak.adapters.jetty.core.JettyRequestAuthenticator;
|
||||||
import org.keycloak.adapters.jetty.core.JettySessionTokenStore;
|
import org.keycloak.adapters.jetty.core.JettySessionTokenStore;
|
||||||
import org.keycloak.adapters.jetty.spi.JettyHttpFacade;
|
import org.keycloak.adapters.jetty.spi.JettyHttpFacade;
|
||||||
|
import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement;
|
||||||
|
|
||||||
import javax.servlet.ServletRequest;
|
import javax.servlet.ServletRequest;
|
||||||
|
|
||||||
@@ -46,6 +47,11 @@ public class KeycloakJettyAuthenticator extends AbstractKeycloakJettyAuthenticat
|
|||||||
return new JettySessionTokenStore(request, resolvedDeployment, new JettyAdapterSessionStore(request));
|
return new JettySessionTokenStore(request, resolvedDeployment, new JettyAdapterSessionStore(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JettyUserSessionManagement createSessionManagement(Request request) {
|
||||||
|
return new JettyUserSessionManagement(new Jetty91SessionManager(request.getSessionManager()));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Request resolveRequest(ServletRequest req) {
|
protected Request resolveRequest(ServletRequest req) {
|
||||||
return (req instanceof Request) ? (Request)req : HttpChannel.getCurrentHttpChannel().getRequest();
|
return (req instanceof Request) ? (Request)req : HttpChannel.getCurrentHttpChannel().getRequest();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.jetty;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.server.SessionManager;
|
||||||
|
import org.keycloak.adapters.jetty.spi.JettySessionManager;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class Jetty92SessionManager implements JettySessionManager {
|
||||||
|
protected SessionManager sessionManager;
|
||||||
|
|
||||||
|
public Jetty92SessionManager(SessionManager sessionManager) {
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpSession getHttpSession(String id) {
|
||||||
|
return sessionManager.getHttpSession(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -27,6 +27,7 @@ import org.keycloak.adapters.jetty.core.AbstractKeycloakJettyAuthenticator;
|
|||||||
import org.keycloak.adapters.jetty.core.JettyRequestAuthenticator;
|
import org.keycloak.adapters.jetty.core.JettyRequestAuthenticator;
|
||||||
import org.keycloak.adapters.jetty.core.JettySessionTokenStore;
|
import org.keycloak.adapters.jetty.core.JettySessionTokenStore;
|
||||||
import org.keycloak.adapters.jetty.spi.JettyHttpFacade;
|
import org.keycloak.adapters.jetty.spi.JettyHttpFacade;
|
||||||
|
import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement;
|
||||||
|
|
||||||
import javax.servlet.ServletRequest;
|
import javax.servlet.ServletRequest;
|
||||||
|
|
||||||
@@ -61,6 +62,11 @@ public class KeycloakJettyAuthenticator extends AbstractKeycloakJettyAuthenticat
|
|||||||
return new JettySessionTokenStore(request, resolvedDeployment, new JettyAdapterSessionStore(request));
|
return new JettySessionTokenStore(request, resolvedDeployment, new JettyAdapterSessionStore(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JettyUserSessionManagement createSessionManagement(Request request) {
|
||||||
|
return new JettyUserSessionManagement(new Jetty92SessionManager(request.getSessionManager()));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected JettyRequestAuthenticator createRequestAuthenticator(Request request, JettyHttpFacade facade,
|
protected JettyRequestAuthenticator createRequestAuthenticator(Request request, JettyHttpFacade facade,
|
||||||
KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
|
KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.jetty;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.server.SessionManager;
|
||||||
|
import org.keycloak.adapters.jetty.spi.JettySessionManager;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class Jetty93SessionManager implements JettySessionManager {
|
||||||
|
protected SessionManager sessionManager;
|
||||||
|
|
||||||
|
public Jetty93SessionManager(SessionManager sessionManager) {
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpSession getHttpSession(String id) {
|
||||||
|
return sessionManager.getHttpSession(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -26,6 +26,7 @@ import org.keycloak.adapters.jetty.core.AbstractKeycloakJettyAuthenticator;
|
|||||||
import org.keycloak.adapters.jetty.core.JettyRequestAuthenticator;
|
import org.keycloak.adapters.jetty.core.JettyRequestAuthenticator;
|
||||||
import org.keycloak.adapters.jetty.core.JettySessionTokenStore;
|
import org.keycloak.adapters.jetty.core.JettySessionTokenStore;
|
||||||
import org.keycloak.adapters.jetty.spi.JettyHttpFacade;
|
import org.keycloak.adapters.jetty.spi.JettyHttpFacade;
|
||||||
|
import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement;
|
||||||
|
|
||||||
import javax.servlet.ServletRequest;
|
import javax.servlet.ServletRequest;
|
||||||
|
|
||||||
@@ -60,6 +61,11 @@ public class KeycloakJettyAuthenticator extends AbstractKeycloakJettyAuthenticat
|
|||||||
return new JettySessionTokenStore(request, resolvedDeployment, new JettyAdapterSessionStore(request));
|
return new JettySessionTokenStore(request, resolvedDeployment, new JettyAdapterSessionStore(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JettyUserSessionManagement createSessionManagement(Request request) {
|
||||||
|
return new JettyUserSessionManagement(new Jetty93SessionManager(request.getSessionManager()));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected JettyRequestAuthenticator createRequestAuthenticator(Request request, JettyHttpFacade facade,
|
protected JettyRequestAuthenticator createRequestAuthenticator(Request request, JettyHttpFacade facade,
|
||||||
KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
|
KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-parent</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>3.2.0.Final</version>
|
||||||
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-jetty94-adapter</artifactId>
|
||||||
|
<name>Keycloak Jetty 9.4.x Integration</name>
|
||||||
|
<properties>
|
||||||
|
<jetty9.version>9.4.2.v20170220</jetty9.version>
|
||||||
|
<keycloak.osgi.export>
|
||||||
|
org.keycloak.adapters.jetty.*
|
||||||
|
</keycloak.osgi.export>
|
||||||
|
<keycloak.osgi.import>
|
||||||
|
org.eclipse.jetty.*;resolution:=optional,
|
||||||
|
javax.servlet.*;version="[3.0,4)";resolution:=optional,
|
||||||
|
org.keycloak.*;version="${project.version}",
|
||||||
|
*;resolution:=optional
|
||||||
|
</keycloak.osgi.import>
|
||||||
|
</properties>
|
||||||
|
<description />
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.logging</groupId>
|
||||||
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
<version>${jboss.logging.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-adapter-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-jetty-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
|
<artifactId>httpclient</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcprov-jdk15on</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-annotations</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-server</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-util</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-security</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<source>1.6</source>
|
||||||
|
<target>1.6</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Adding OSGI metadata to the JAR without changing the packaging type. -->
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.felix</groupId>
|
||||||
|
<artifactId>maven-bundle-plugin</artifactId>
|
||||||
|
<extensions>true</extensions>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>bundle-manifest</id>
|
||||||
|
<phase>process-classes</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>manifest</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<instructions>
|
||||||
|
<Bundle-ClassPath>.</Bundle-ClassPath>
|
||||||
|
<Bundle-Name>${project.name}</Bundle-Name>
|
||||||
|
<Bundle-SymbolicName>${project.groupId}.${project.artifactId}</Bundle-SymbolicName>
|
||||||
|
<Import-Package>${keycloak.osgi.import}</Import-Package>
|
||||||
|
<Export-Package>${keycloak.osgi.export}</Export-Package>
|
||||||
|
</instructions>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.jetty;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.server.Request;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.jetty.core.JettyRequestAuthenticator;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class Jetty94RequestAuthenticator extends JettyRequestAuthenticator {
|
||||||
|
public Jetty94RequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore, int sslRedirectPort, Request request) {
|
||||||
|
super(facade, deployment, tokenStore, sslRedirectPort, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String changeHttpSessionId(boolean create) {
|
||||||
|
Request request = this.request;
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
if (session == null) {
|
||||||
|
return request.getSession(true).getId();
|
||||||
|
}
|
||||||
|
if (!deployment.isTurnOffChangeSessionIdOnLogin()) return request.changeSessionId();
|
||||||
|
else return session.getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.jetty;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.server.session.SessionHandler;
|
||||||
|
import org.keycloak.adapters.jetty.spi.JettySessionManager;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class Jetty94SessionManager implements JettySessionManager {
|
||||||
|
protected SessionHandler sessionHandler;
|
||||||
|
|
||||||
|
public Jetty94SessionManager(SessionHandler sessionHandler) {
|
||||||
|
this.sessionHandler = sessionHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpSession getHttpSession(String id) {
|
||||||
|
return sessionHandler.getHttpSession(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.jetty;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.security.authentication.FormAuthenticator;
|
||||||
|
import org.eclipse.jetty.server.Request;
|
||||||
|
import org.eclipse.jetty.util.MultiMap;
|
||||||
|
import org.keycloak.adapters.jetty.spi.JettyHttpFacade;
|
||||||
|
import org.keycloak.adapters.spi.AdapterSessionStore;
|
||||||
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class JettyAdapterSessionStore implements AdapterSessionStore {
|
||||||
|
public static final String CACHED_FORM_PARAMETERS = "__CACHED_FORM_PARAMETERS";
|
||||||
|
protected Request myRequest;
|
||||||
|
|
||||||
|
public JettyAdapterSessionStore(Request request) {
|
||||||
|
this.myRequest = request; // for IDE/compilation purposes
|
||||||
|
}
|
||||||
|
|
||||||
|
protected MultiMap<String> extractFormParameters(Request base_request) {
|
||||||
|
MultiMap<String> formParameters = new MultiMap<String>();
|
||||||
|
base_request.extractFormParameters(formParameters);
|
||||||
|
return formParameters;
|
||||||
|
}
|
||||||
|
protected void restoreFormParameters(MultiMap<String> j_post, Request base_request) {
|
||||||
|
base_request.setContentParameters(j_post);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean restoreRequest() {
|
||||||
|
HttpSession session = myRequest.getSession(false);
|
||||||
|
if (session == null) return false;
|
||||||
|
synchronized (session) {
|
||||||
|
String j_uri = (String) session.getAttribute(FormAuthenticator.__J_URI);
|
||||||
|
if (j_uri != null) {
|
||||||
|
// check if the request is for the same url as the original and restore
|
||||||
|
// params if it was a post
|
||||||
|
StringBuffer buf = myRequest.getRequestURL();
|
||||||
|
if (myRequest.getQueryString() != null)
|
||||||
|
buf.append("?").append(myRequest.getQueryString());
|
||||||
|
if (j_uri.equals(buf.toString())) {
|
||||||
|
String method = (String)session.getAttribute(JettyHttpFacade.__J_METHOD);
|
||||||
|
myRequest.setMethod(method);
|
||||||
|
MultivaluedHashMap<String, String> j_post = (MultivaluedHashMap<String, String>) session.getAttribute(CACHED_FORM_PARAMETERS);
|
||||||
|
if (j_post != null) {
|
||||||
|
myRequest.setContentType("application/x-www-form-urlencoded");
|
||||||
|
MultiMap<String> map = new MultiMap<String>();
|
||||||
|
for (String key : j_post.keySet()) {
|
||||||
|
for (String val : j_post.getList(key)) {
|
||||||
|
map.add(key, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
restoreFormParameters(map, myRequest);
|
||||||
|
}
|
||||||
|
session.removeAttribute(FormAuthenticator.__J_URI);
|
||||||
|
session.removeAttribute(JettyHttpFacade.__J_METHOD);
|
||||||
|
session.removeAttribute(FormAuthenticator.__J_POST);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveRequest() {
|
||||||
|
// remember the current URI
|
||||||
|
HttpSession session = myRequest.getSession();
|
||||||
|
synchronized (session) {
|
||||||
|
// But only if it is not set already, or we save every uri that leads to a login form redirect
|
||||||
|
if (session.getAttribute(FormAuthenticator.__J_URI) == null) {
|
||||||
|
StringBuffer buf = myRequest.getRequestURL();
|
||||||
|
if (myRequest.getQueryString() != null)
|
||||||
|
buf.append("?").append(myRequest.getQueryString());
|
||||||
|
session.setAttribute(FormAuthenticator.__J_URI, buf.toString());
|
||||||
|
session.setAttribute(JettyHttpFacade.__J_METHOD, myRequest.getMethod());
|
||||||
|
|
||||||
|
if ("application/x-www-form-urlencoded".equals(myRequest.getContentType()) && "POST".equalsIgnoreCase(myRequest.getMethod())) {
|
||||||
|
MultiMap<String> formParameters = extractFormParameters(myRequest);
|
||||||
|
MultivaluedHashMap<String, String> map = new MultivaluedHashMap<String, String>();
|
||||||
|
for (String key : formParameters.keySet()) {
|
||||||
|
for (Object value : formParameters.getValues(key)) {
|
||||||
|
map.add(key, (String) value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session.setAttribute(CACHED_FORM_PARAMETERS, map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.jetty;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.server.Authentication;
|
||||||
|
import org.eclipse.jetty.server.Request;
|
||||||
|
import org.eclipse.jetty.server.UserIdentity;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.jetty.core.AbstractKeycloakJettyAuthenticator;
|
||||||
|
import org.keycloak.adapters.jetty.core.JettyRequestAuthenticator;
|
||||||
|
import org.keycloak.adapters.jetty.core.JettySessionTokenStore;
|
||||||
|
import org.keycloak.adapters.jetty.spi.JettyHttpFacade;
|
||||||
|
import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement;
|
||||||
|
|
||||||
|
import javax.servlet.ServletRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class KeycloakJettyAuthenticator extends AbstractKeycloakJettyAuthenticator {
|
||||||
|
|
||||||
|
public KeycloakJettyAuthenticator() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Request resolveRequest(ServletRequest req) {
|
||||||
|
return Request.getBaseRequest(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Authentication createAuthentication(UserIdentity userIdentity, final Request request) {
|
||||||
|
return new KeycloakAuthentication(getAuthMethod(), userIdentity) {
|
||||||
|
@Override
|
||||||
|
public void logout() {
|
||||||
|
logoutCurrent(request);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment) {
|
||||||
|
return new JettySessionTokenStore(request, resolvedDeployment, new JettyAdapterSessionStore(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JettyUserSessionManagement createSessionManagement(Request request) {
|
||||||
|
return new JettyUserSessionManagement(new Jetty94SessionManager(request.getSessionHandler()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected JettyRequestAuthenticator createRequestAuthenticator(Request request, JettyHttpFacade facade,
|
||||||
|
KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
|
||||||
|
return new Jetty94RequestAuthenticator(facade, deployment, tokenStore, -1, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<name>Keycloak Jetty Integration</name>
|
<name>Keycloak Jetty Integration</name>
|
||||||
@@ -33,8 +33,22 @@
|
|||||||
<modules>
|
<modules>
|
||||||
<module>jetty-core</module>
|
<module>jetty-core</module>
|
||||||
<module>jetty8.1</module>
|
<module>jetty8.1</module>
|
||||||
<module>jetty9.1</module>
|
|
||||||
<module>jetty9.2</module>
|
<module>jetty9.2</module>
|
||||||
<module>jetty9.3</module>
|
<module>jetty9.3</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>community</id>
|
||||||
|
<activation>
|
||||||
|
<property>
|
||||||
|
<name>!product</name>
|
||||||
|
</property>
|
||||||
|
</activation>
|
||||||
|
<modules>
|
||||||
|
<module>jetty9.1</module>
|
||||||
|
<module>jetty9.4</module>
|
||||||
|
</modules>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
</project>
|
</project>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright 2017 Brett Epps <https://github.com/eppsilon>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
* associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
* following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
* portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
|
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||||
|
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import * as Keycloak from 'keycloak';
|
||||||
|
|
||||||
|
export as namespace KeycloakAuthorization;
|
||||||
|
|
||||||
|
export = KeycloakAuthorization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Keycloak client instance.
|
||||||
|
* @param config Path to a JSON config file or a plain config object.
|
||||||
|
*/
|
||||||
|
declare function KeycloakAuthorization(keycloak: Keycloak.KeycloakInstance): KeycloakAuthorization.KeycloakAuthorizationInstance;
|
||||||
|
|
||||||
|
declare namespace KeycloakAuthorization {
|
||||||
|
interface KeycloakAuthorizationPromise {
|
||||||
|
then(onGrant: (rpt: string) => void, onDeny: () => void, onError: () => void): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeycloakAuthorizationInstance {
|
||||||
|
rpt: any;
|
||||||
|
config: { rpt_endpoint: string };
|
||||||
|
|
||||||
|
init(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method enables client applications to better integrate with resource servers protected by a Keycloak
|
||||||
|
* policy enforcer.
|
||||||
|
*
|
||||||
|
* In this case, the resource server will respond with a 401 status code and a WWW-Authenticate header holding the
|
||||||
|
* necessary information to ask a Keycloak server for authorization data using both UMA and Entitlement protocol,
|
||||||
|
* depending on how the policy enforcer at the resource server was configured.
|
||||||
|
*/
|
||||||
|
authorize(wwwAuthenticateHeader: string): KeycloakAuthorizationPromise;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains all entitlements from a Keycloak server based on a given resourceServerId.
|
||||||
|
*/
|
||||||
|
entitlement(resourceServerId: string, entitlementRequest: {}): KeycloakAuthorizationPromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
+478
@@ -0,0 +1,478 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright 2017 Brett Epps <https://github.com/eppsilon>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
* associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
* following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
* portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
|
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||||
|
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
export as namespace Keycloak;
|
||||||
|
|
||||||
|
export = Keycloak;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Keycloak client instance.
|
||||||
|
* @param config Path to a JSON config file or a plain config object.
|
||||||
|
*/
|
||||||
|
declare function Keycloak(config?: string|{}): Keycloak.KeycloakInstance;
|
||||||
|
|
||||||
|
declare namespace Keycloak {
|
||||||
|
type KeycloakAdapterName = 'cordova'|'default';
|
||||||
|
type KeycloakOnLoad = 'login-required'|'check-sso';
|
||||||
|
type KeycloakResponseMode = 'query'|'fragment';
|
||||||
|
type KeycloakResponseType = 'code'|'id_token token'|'code id_token token';
|
||||||
|
type KeycloakFlow = 'standard'|'implicit'|'hybrid';
|
||||||
|
|
||||||
|
interface KeycloakInitOptions {
|
||||||
|
/**
|
||||||
|
* @private Undocumented.
|
||||||
|
*/
|
||||||
|
adapter?: KeycloakAdapterName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies an action to do on load.
|
||||||
|
*/
|
||||||
|
onLoad?: KeycloakOnLoad;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an initial value for the token.
|
||||||
|
*/
|
||||||
|
token?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an initial value for the refresh token.
|
||||||
|
*/
|
||||||
|
refreshToken?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an initial value for the id token (only together with `token` or
|
||||||
|
* `refreshToken`).
|
||||||
|
*/
|
||||||
|
idToken?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an initial value for skew between local time and Keycloak server in
|
||||||
|
* seconds (only together with `token` or `refreshToken`).
|
||||||
|
*/
|
||||||
|
timeSkew?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to enable/disable monitoring login state.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
checkLoginIframe?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the interval to check login state (in seconds).
|
||||||
|
* @default 5
|
||||||
|
*/
|
||||||
|
checkLoginIframeInterval?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the OpenID Connect response mode to send to Keycloak upon login.
|
||||||
|
* @default fragment After successful authentication Keycloak will redirect
|
||||||
|
* to JavaScript application with OpenID Connect parameters
|
||||||
|
* added in URL fragment. This is generally safer and
|
||||||
|
* recommended over query.
|
||||||
|
*/
|
||||||
|
responseMode?: KeycloakResponseMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the OpenID Connect flow.
|
||||||
|
* @default standard
|
||||||
|
*/
|
||||||
|
flow?: KeycloakFlow;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeycloakLoginOptions {
|
||||||
|
/**
|
||||||
|
* @private Undocumented.
|
||||||
|
*/
|
||||||
|
scope?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the uri to redirect to after login.
|
||||||
|
*/
|
||||||
|
redirectUri?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default the login screen is displayed if the user is not logged into
|
||||||
|
* Keycloak. To only authenticate to the application if the user is already
|
||||||
|
* logged in and not display the login page if the user is not logged in, set
|
||||||
|
* this option to `'none'`. To always require re-authentication and ignore
|
||||||
|
* SSO, set this option to `'login'`.
|
||||||
|
*/
|
||||||
|
prompt?: 'none'|'login';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If value is `'register'` then user is redirected to registration page,
|
||||||
|
* otherwise to login page.
|
||||||
|
*/
|
||||||
|
action?: 'register';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used just if user is already authenticated. Specifies maximum time since
|
||||||
|
* the authentication of user happened. If user is already authenticated for
|
||||||
|
* longer time than `'maxAge'`, the SSO is ignored and he will need to
|
||||||
|
* authenticate again.
|
||||||
|
*/
|
||||||
|
maxAge?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to pre-fill the username/email field on the login form.
|
||||||
|
*/
|
||||||
|
loginHint?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to tell Keycloak which IDP the user wants to authenticate with.
|
||||||
|
*/
|
||||||
|
idpHint?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the desired locale for the UI.
|
||||||
|
*/
|
||||||
|
locale?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeycloakPromiseCallback<T> = (result: T) => void;
|
||||||
|
|
||||||
|
interface KeycloakPromise<TSuccess, TError> {
|
||||||
|
/**
|
||||||
|
* Function to call if the promised action succeeds.
|
||||||
|
*/
|
||||||
|
success(callback: KeycloakPromiseCallback<TSuccess>): KeycloakPromise<TSuccess, TError>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to call if the promised action throws an error.
|
||||||
|
*/
|
||||||
|
error(callback: KeycloakPromiseCallback<TError>): KeycloakPromise<TSuccess, TError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeycloakError {
|
||||||
|
error: string;
|
||||||
|
error_description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeycloakAdapter {
|
||||||
|
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||||
|
logout(options?: any): KeycloakPromise<void, void>;
|
||||||
|
register(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||||
|
accountManagement(): KeycloakPromise<void, void>;
|
||||||
|
redirectUri(options: { redirectUri: string; }, encodeHash: boolean): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeycloakProfile {
|
||||||
|
id?: string;
|
||||||
|
username?: string;
|
||||||
|
email?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
emailVerified?: boolean;
|
||||||
|
totp?: boolean;
|
||||||
|
createdTimestamp?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// export interface KeycloakUserInfo {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A client for the Keycloak authentication server.
|
||||||
|
* @see {@link https://keycloak.gitbooks.io/securing-client-applications-guide/content/topics/oidc/javascript-adapter.html|Keycloak JS adapter documentation}
|
||||||
|
*/
|
||||||
|
interface KeycloakInstance {
|
||||||
|
/**
|
||||||
|
* Is true if the user is authenticated, false otherwise.
|
||||||
|
*/
|
||||||
|
authenticated?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user id.
|
||||||
|
*/
|
||||||
|
subject?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response mode passed in init (default value is `'fragment'`).
|
||||||
|
*/
|
||||||
|
responseMode?: KeycloakResponseMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response type sent to Keycloak with login requests. This is determined
|
||||||
|
* based on the flow value used during initialization, but can be overridden
|
||||||
|
* by setting this value.
|
||||||
|
*/
|
||||||
|
responseType?: KeycloakResponseType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flow passed in init.
|
||||||
|
*/
|
||||||
|
flow?: KeycloakFlow;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The realm roles associated with the token.
|
||||||
|
*/
|
||||||
|
realmAccess?: { roles: string[] };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource roles associated with the token.
|
||||||
|
*/
|
||||||
|
resourceAccess?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base64 encoded token that can be sent in the Authorization header in
|
||||||
|
* requests to services.
|
||||||
|
*/
|
||||||
|
token?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parsed token as a JavaScript object.
|
||||||
|
*/
|
||||||
|
tokenParsed?: {
|
||||||
|
exp?: number;
|
||||||
|
iat?: number;
|
||||||
|
nonce?: string;
|
||||||
|
sub?: string;
|
||||||
|
session_state?: string;
|
||||||
|
realm_access?: { roles: string[] };
|
||||||
|
resource_access?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base64 encoded refresh token that can be used to retrieve a new token.
|
||||||
|
*/
|
||||||
|
refreshToken?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parsed refresh token as a JavaScript object.
|
||||||
|
*/
|
||||||
|
refreshTokenParsed?: { nonce?: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base64 encoded ID token.
|
||||||
|
*/
|
||||||
|
idToken?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parsed id token as a JavaScript object.
|
||||||
|
*/
|
||||||
|
idTokenParsed?: { nonce?: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The estimated time difference between the browser time and the Keycloak
|
||||||
|
* server in seconds. This value is just an estimation, but is accurate
|
||||||
|
* enough when determining if a token is expired or not.
|
||||||
|
*/
|
||||||
|
timeSkew?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private Undocumented.
|
||||||
|
*/
|
||||||
|
loginRequired?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private Undocumented.
|
||||||
|
*/
|
||||||
|
authServerUrl?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private Undocumented.
|
||||||
|
*/
|
||||||
|
realm?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private Undocumented.
|
||||||
|
*/
|
||||||
|
clientId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private Undocumented.
|
||||||
|
*/
|
||||||
|
clientSecret?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private Undocumented.
|
||||||
|
*/
|
||||||
|
redirectUri?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private Undocumented.
|
||||||
|
*/
|
||||||
|
sessionId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private Undocumented.
|
||||||
|
*/
|
||||||
|
profile?: KeycloakProfile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private Undocumented.
|
||||||
|
*/
|
||||||
|
userInfo?: {}; // KeycloakUserInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the adapter is initialized.
|
||||||
|
*/
|
||||||
|
onReady?(authenticated?: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a user is successfully authenticated.
|
||||||
|
*/
|
||||||
|
onAuthSuccess?(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called if there was an error during authentication.
|
||||||
|
*/
|
||||||
|
onAuthError?(errorData: KeycloakError): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the token is refreshed.
|
||||||
|
*/
|
||||||
|
onAuthRefreshSuccess?(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called if there was an error while trying to refresh the token.
|
||||||
|
*/
|
||||||
|
onAuthRefreshError?(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called if the user is logged out (will only be called if the session
|
||||||
|
* status iframe is enabled, or in Cordova mode).
|
||||||
|
*/
|
||||||
|
onAuthLogout?(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the access token is expired. If a refresh token is available
|
||||||
|
* the token can be refreshed with Keycloak#updateToken, or in cases where
|
||||||
|
* it's not (ie. with implicit flow) you can redirect to login screen to
|
||||||
|
* obtain a new access token.
|
||||||
|
*/
|
||||||
|
onTokenExpired?(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to initialize the adapter.
|
||||||
|
* @param initOptions Initialization options.
|
||||||
|
* @returns A promise to set functions to be invoked on success or error.
|
||||||
|
*/
|
||||||
|
init(initOptions: KeycloakInitOptions): KeycloakPromise<boolean, KeycloakError>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects to login form.
|
||||||
|
* @param options Login options.
|
||||||
|
*/
|
||||||
|
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects to logout.
|
||||||
|
* @param options Logout options.
|
||||||
|
* @param options.redirectUri Specifies the uri to redirect to after logout.
|
||||||
|
*/
|
||||||
|
logout(options?: any): KeycloakPromise<void, void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects to registration form.
|
||||||
|
* @param options Supports same options as Keycloak#login but `action` is
|
||||||
|
* set to `'register'`.
|
||||||
|
*/
|
||||||
|
register(options?: any): KeycloakPromise<void, void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects to the Account Management Console.
|
||||||
|
*/
|
||||||
|
accountManagement(): KeycloakPromise<void, void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the URL to login form.
|
||||||
|
* @param options Supports same options as Keycloak#login.
|
||||||
|
*/
|
||||||
|
createLoginUrl(options?: KeycloakLoginOptions): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the URL to logout the user.
|
||||||
|
* @param options Logout options.
|
||||||
|
* @param options.redirectUri Specifies the uri to redirect to after logout.
|
||||||
|
*/
|
||||||
|
createLogoutUrl(options?: any): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the URL to registration page.
|
||||||
|
* @param options Supports same options as Keycloak#createLoginUrl but
|
||||||
|
* `action` is set to `'register'`.
|
||||||
|
*/
|
||||||
|
createRegisterUrl(options?: KeycloakLoginOptions): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the URL to the Account Management Console.
|
||||||
|
*/
|
||||||
|
createAccountUrl(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the token has less than `minValidity` seconds left before
|
||||||
|
* it expires.
|
||||||
|
* @param minValidity If not specified, `0` is used.
|
||||||
|
*/
|
||||||
|
isTokenExpired(minValidity?: number): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the token expires within `minValidity` seconds, the token is refreshed.
|
||||||
|
* If the session status iframe is enabled, the session status is also
|
||||||
|
* checked.
|
||||||
|
* @returns A promise to set functions that can be invoked if the token is
|
||||||
|
* still valid, or if the token is no longer valid.
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* keycloak.updateToken(5).success(function(refreshed) {
|
||||||
|
* if (refreshed) {
|
||||||
|
* alert('Token was successfully refreshed');
|
||||||
|
* } else {
|
||||||
|
* alert('Token is still valid');
|
||||||
|
* }
|
||||||
|
* }).error(function() {
|
||||||
|
* alert('Failed to refresh the token, or the session has expired');
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
updateToken(minValidity: number): KeycloakPromise<boolean, boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears authentication state, including tokens. This can be useful if
|
||||||
|
* the application has detected the session was expired, for example if
|
||||||
|
* updating token fails. Invoking this results in Keycloak#onAuthLogout
|
||||||
|
* callback listener being invoked.
|
||||||
|
*/
|
||||||
|
clearToken(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the token has the given realm role.
|
||||||
|
* @param role A realm role name.
|
||||||
|
*/
|
||||||
|
hasRealmRole(role: string): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the token has the given role for the resource.
|
||||||
|
* @param role A role name.
|
||||||
|
* @param resource If not specified, `clientId` is used.
|
||||||
|
*/
|
||||||
|
hasResourceRole(role: string, resource?: string): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the user's profile.
|
||||||
|
* @returns A promise to set functions to be invoked on success or error.
|
||||||
|
*/
|
||||||
|
loadUserProfile(): KeycloakPromise<KeycloakProfile, void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private Undocumented.
|
||||||
|
*/
|
||||||
|
loadUserInfo(): KeycloakPromise<{}, void>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -87,6 +87,10 @@
|
|||||||
}
|
}
|
||||||
kc.flow = initOptions.flow;
|
kc.flow = initOptions.flow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (initOptions.timeSkew != null) {
|
||||||
|
kc.timeSkew = initOptions.timeSkew;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!kc.responseMode) {
|
if (!kc.responseMode) {
|
||||||
@@ -153,7 +157,7 @@
|
|||||||
processCallback(callback, initPromise);
|
processCallback(callback, initPromise);
|
||||||
return;
|
return;
|
||||||
} else if (initOptions) {
|
} else if (initOptions) {
|
||||||
if (initOptions.token || initOptions.refreshToken) {
|
if (initOptions.token && initOptions.refreshToken) {
|
||||||
setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken);
|
setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken);
|
||||||
|
|
||||||
if (loginIframe.enable) {
|
if (loginIframe.enable) {
|
||||||
@@ -162,12 +166,8 @@
|
|||||||
kc.onAuthSuccess && kc.onAuthSuccess();
|
kc.onAuthSuccess && kc.onAuthSuccess();
|
||||||
initPromise.setSuccess();
|
initPromise.setSuccess();
|
||||||
}).error(function () {
|
}).error(function () {
|
||||||
kc.onAuthError && kc.onAuthError();
|
setToken(null, null, null);
|
||||||
if (initOptions.onLoad) {
|
initPromise.setSuccess();
|
||||||
onLoad();
|
|
||||||
} else {
|
|
||||||
initPromise.setError();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -369,6 +369,11 @@
|
|||||||
throw 'Not authenticated';
|
throw 'Not authenticated';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (kc.timeSkew == null) {
|
||||||
|
console.info('[KEYCLOAK] Unable to determine if token is expired as timeskew is not set');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
var expiresIn = kc.tokenParsed['exp'] - Math.ceil(new Date().getTime() / 1000) + kc.timeSkew;
|
var expiresIn = kc.tokenParsed['exp'] - Math.ceil(new Date().getTime() / 1000) + kc.timeSkew;
|
||||||
if (minValidity) {
|
if (minValidity) {
|
||||||
expiresIn -= minValidity;
|
expiresIn -= minValidity;
|
||||||
@@ -379,7 +384,7 @@
|
|||||||
kc.updateToken = function(minValidity) {
|
kc.updateToken = function(minValidity) {
|
||||||
var promise = createPromise();
|
var promise = createPromise();
|
||||||
|
|
||||||
if (!kc.tokenParsed || !kc.refreshToken) {
|
if (!kc.refreshToken) {
|
||||||
promise.setError();
|
promise.setError();
|
||||||
return promise.promise;
|
return promise.promise;
|
||||||
}
|
}
|
||||||
@@ -388,14 +393,10 @@
|
|||||||
|
|
||||||
var exec = function() {
|
var exec = function() {
|
||||||
var refreshToken = false;
|
var refreshToken = false;
|
||||||
if (kc.timeSkew == -1) {
|
if (minValidity == -1) {
|
||||||
console.info('Skew ' + kc.timeSkew);
|
|
||||||
refreshToken = true;
|
|
||||||
console.info('[KEYCLOAK] Refreshing token: time skew not set');
|
|
||||||
} else if (minValidity == -1) {
|
|
||||||
refreshToken = true;
|
refreshToken = true;
|
||||||
console.info('[KEYCLOAK] Refreshing token: forced refresh');
|
console.info('[KEYCLOAK] Refreshing token: forced refresh');
|
||||||
} else if (kc.isTokenExpired(minValidity)) {
|
} else if (!kc.tokenParsed || kc.isTokenExpired(minValidity)) {
|
||||||
refreshToken = true;
|
refreshToken = true;
|
||||||
console.info('[KEYCLOAK] Refreshing token: token expired');
|
console.info('[KEYCLOAK] Refreshing token: token expired');
|
||||||
}
|
}
|
||||||
@@ -586,7 +587,7 @@
|
|||||||
|
|
||||||
req.onreadystatechange = function () {
|
req.onreadystatechange = function () {
|
||||||
if (req.readyState == 4) {
|
if (req.readyState == 4) {
|
||||||
if (req.status == 200) {
|
if (req.status == 200 || fileLoaded(req)) {
|
||||||
var config = JSON.parse(req.responseText);
|
var config = JSON.parse(req.responseText);
|
||||||
|
|
||||||
kc.authServerUrl = config['auth-server-url'];
|
kc.authServerUrl = config['auth-server-url'];
|
||||||
@@ -632,55 +633,16 @@
|
|||||||
return promise.promise;
|
return promise.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fileLoaded(xhr) {
|
||||||
|
return xhr.status == 0 && xhr.responseText && xhr.responseURL.startsWith('file:');
|
||||||
|
}
|
||||||
|
|
||||||
function setToken(token, refreshToken, idToken, timeLocal) {
|
function setToken(token, refreshToken, idToken, timeLocal) {
|
||||||
if (kc.tokenTimeoutHandle) {
|
if (kc.tokenTimeoutHandle) {
|
||||||
clearTimeout(kc.tokenTimeoutHandle);
|
clearTimeout(kc.tokenTimeoutHandle);
|
||||||
kc.tokenTimeoutHandle = null;
|
kc.tokenTimeoutHandle = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token) {
|
|
||||||
kc.token = token;
|
|
||||||
kc.tokenParsed = decodeToken(token);
|
|
||||||
var sessionId = kc.realm + '/' + kc.tokenParsed.sub;
|
|
||||||
if (kc.tokenParsed.session_state) {
|
|
||||||
sessionId = sessionId + '/' + kc.tokenParsed.session_state;
|
|
||||||
}
|
|
||||||
kc.sessionId = sessionId;
|
|
||||||
kc.authenticated = true;
|
|
||||||
kc.subject = kc.tokenParsed.sub;
|
|
||||||
kc.realmAccess = kc.tokenParsed.realm_access;
|
|
||||||
kc.resourceAccess = kc.tokenParsed.resource_access;
|
|
||||||
|
|
||||||
if (timeLocal) {
|
|
||||||
kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat;
|
|
||||||
console.info('[KEYCLOAK] Estimated time difference between browser and server is ' + kc.timeSkew + ' seconds');
|
|
||||||
} else {
|
|
||||||
kc.timeSkew = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (kc.onTokenExpired) {
|
|
||||||
if (kc.timeSkew == -1) {
|
|
||||||
kc.onTokenExpired();
|
|
||||||
} else {
|
|
||||||
var expiresIn = (kc.tokenParsed['exp'] - (new Date().getTime() / 1000) + kc.timeSkew) * 1000;
|
|
||||||
if (expiresIn <= 0) {
|
|
||||||
kc.onTokenExpired();
|
|
||||||
} else {
|
|
||||||
kc.tokenTimeoutHandle = setTimeout(kc.onTokenExpired, expiresIn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
delete kc.token;
|
|
||||||
delete kc.tokenParsed;
|
|
||||||
delete kc.subject;
|
|
||||||
delete kc.realmAccess;
|
|
||||||
delete kc.resourceAccess;
|
|
||||||
|
|
||||||
kc.authenticated = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
kc.refreshToken = refreshToken;
|
kc.refreshToken = refreshToken;
|
||||||
kc.refreshTokenParsed = decodeToken(refreshToken);
|
kc.refreshTokenParsed = decodeToken(refreshToken);
|
||||||
@@ -696,6 +658,42 @@
|
|||||||
delete kc.idToken;
|
delete kc.idToken;
|
||||||
delete kc.idTokenParsed;
|
delete kc.idTokenParsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
kc.token = token;
|
||||||
|
kc.tokenParsed = decodeToken(token);
|
||||||
|
kc.sessionId = kc.tokenParsed.session_state;
|
||||||
|
kc.authenticated = true;
|
||||||
|
kc.subject = kc.tokenParsed.sub;
|
||||||
|
kc.realmAccess = kc.tokenParsed.realm_access;
|
||||||
|
kc.resourceAccess = kc.tokenParsed.resource_access;
|
||||||
|
|
||||||
|
if (timeLocal) {
|
||||||
|
kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kc.timeSkew != null) {
|
||||||
|
console.info('[KEYCLOAK] Estimated time difference between browser and server is ' + kc.timeSkew + ' seconds');
|
||||||
|
|
||||||
|
if (kc.onTokenExpired) {
|
||||||
|
var expiresIn = (kc.tokenParsed['exp'] - (new Date().getTime() / 1000) + kc.timeSkew) * 1000;
|
||||||
|
console.info('[KEYCLOAK] Token expires in ' + Math.round(expiresIn / 1000) + ' s');
|
||||||
|
if (expiresIn <= 0) {
|
||||||
|
kc.onTokenExpired();
|
||||||
|
} else {
|
||||||
|
kc.tokenTimeoutHandle = setTimeout(kc.onTokenExpired, expiresIn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete kc.token;
|
||||||
|
delete kc.tokenParsed;
|
||||||
|
delete kc.subject;
|
||||||
|
delete kc.realmAccess;
|
||||||
|
delete kc.resourceAccess;
|
||||||
|
|
||||||
|
kc.authenticated = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeToken(str) {
|
function decodeToken(str) {
|
||||||
@@ -838,22 +836,28 @@
|
|||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
|
|
||||||
var messageCallback = function(event) {
|
var messageCallback = function(event) {
|
||||||
if (event.origin !== loginIframe.iframeOrigin) {
|
if ((event.origin !== loginIframe.iframeOrigin) || (loginIframe.iframe.contentWindow !== event.source)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.data != "unchanged") {
|
if (!(event.data == 'unchanged' || event.data == 'changed' || event.data == 'error')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (event.data != 'unchanged') {
|
||||||
kc.clearToken();
|
kc.clearToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = loginIframe.callbackList.length - 1; i >= 0; --i) {
|
var callbacks = loginIframe.callbackList.splice(0, loginIframe.callbackList.length);
|
||||||
var promise = loginIframe.callbackList[i];
|
|
||||||
if (event.data == "unchanged") {
|
for (var i = callbacks.length - 1; i >= 0; --i) {
|
||||||
|
var promise = callbacks[i];
|
||||||
|
if (event.data == 'unchanged') {
|
||||||
promise.setSuccess();
|
promise.setSuccess();
|
||||||
} else {
|
} else {
|
||||||
promise.setError();
|
promise.setError();
|
||||||
}
|
}
|
||||||
loginIframe.callbackList.splice(i, 1);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1228,7 +1232,7 @@
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (responseMode != 'query' || !handleQueryParam(param, queryParams[param], oauth)) {
|
if (responseMode != 'query' || !handleQueryParam(param, queryParams[param], oauth)) {
|
||||||
oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + param + '=' + queryParams[param];
|
oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + param + '=' + encodeURIComponent(queryParams[param]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,14 +35,14 @@
|
|||||||
req.open('GET', url, true);
|
req.open('GET', url, true);
|
||||||
|
|
||||||
req.onreadystatechange = function () {
|
req.onreadystatechange = function () {
|
||||||
if (req.readyState == 4) {
|
if (req.readyState === 4) {
|
||||||
if (req.status == 204) {
|
if (req.status === 204) {
|
||||||
init = {
|
init = {
|
||||||
clientId: clientId,
|
clientId: clientId,
|
||||||
origin: origin
|
origin: origin
|
||||||
}
|
}
|
||||||
callback('unchanged');
|
callback('unchanged');
|
||||||
} else if (req.status = 404) {
|
} else if (req.status === 404) {
|
||||||
callback('changed');
|
callback('changed');
|
||||||
} else {
|
} else {
|
||||||
callback('error');
|
callback('error');
|
||||||
@@ -52,8 +52,9 @@
|
|||||||
|
|
||||||
req.send();
|
req.send();
|
||||||
} else {
|
} else {
|
||||||
if (clientId == init.clientId && origin == init.origin) {
|
if (clientId === init.clientId && origin === init.origin) {
|
||||||
if (sessionState == cookie) {
|
var c = cookie.split('/');
|
||||||
|
if (sessionState === c[2]) {
|
||||||
callback('unchanged');
|
callback('unchanged');
|
||||||
} else {
|
} else {
|
||||||
callback('changed');
|
callback('changed');
|
||||||
@@ -71,7 +72,7 @@
|
|||||||
for(var i=0; i<ca.length; i++)
|
for(var i=0; i<ca.length; i++)
|
||||||
{
|
{
|
||||||
var c = ca[i].trim();
|
var c = ca[i].trim();
|
||||||
if (c.indexOf(name)==0) return c.substring(name.length,c.length);
|
if (c.indexOf(name)===0) return c.substring(name.length,c.length);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
var origin = event.origin;
|
var origin = event.origin;
|
||||||
var data = event.data.split(' ');
|
var data = event.data.split(' ');
|
||||||
if (data.length != 2) {
|
if (data.length != 2) {
|
||||||
event.source.postMessage('error', origin);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientId = data[0];
|
var clientId = data[0];
|
||||||
@@ -95,4 +96,4 @@
|
|||||||
window.addEventListener("message", receiveMessage, false);
|
window.addEventListener("message", receiveMessage, false);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.osgi;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.KeycloakConfigResolver;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.osgi.framework.BundleContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class BundleBasedKeycloakConfigResolver implements KeycloakConfigResolver {
|
||||||
|
|
||||||
|
private volatile KeycloakDeployment cachedDeployment;
|
||||||
|
|
||||||
|
private BundleContext bundleContext;
|
||||||
|
private String configLocation = "WEB-INF/keycloak.json";
|
||||||
|
|
||||||
|
public BundleContext getBundleContext() {
|
||||||
|
return bundleContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBundleContext(BundleContext bundleContext) {
|
||||||
|
this.bundleContext = bundleContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConfigLocation() {
|
||||||
|
return configLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfigLocation(String configLocation) {
|
||||||
|
this.configLocation = configLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeycloakDeployment resolve(HttpFacade.Request request) {
|
||||||
|
if (cachedDeployment != null) {
|
||||||
|
return cachedDeployment;
|
||||||
|
} else {
|
||||||
|
cachedDeployment = findDeployment(request);
|
||||||
|
return cachedDeployment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected KeycloakDeployment findDeployment(HttpFacade.Request request) {
|
||||||
|
if (bundleContext == null) {
|
||||||
|
throw new IllegalStateException("bundleContext must be set for BundleBasedKeycloakConfigResolver!");
|
||||||
|
}
|
||||||
|
|
||||||
|
URL url = bundleContext.getBundle().getResource(configLocation);
|
||||||
|
if (url == null) {
|
||||||
|
throw new IllegalStateException("Failed to find the file " + configLocation + " on classpath.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
InputStream is = url.openStream();
|
||||||
|
return KeycloakDeploymentBuilder.build(is);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new IllegalStateException("Error reading file' " + configLocation + "' from bundle classpath.", ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../pom.xml</relativePath>
|
<relativePath>../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<name>Keycloak OIDC Client Adapter Modules</name>
|
<name>Keycloak OIDC Client Adapter Modules</name>
|
||||||
@@ -41,9 +41,11 @@
|
|||||||
<module>servlet-filter</module>
|
<module>servlet-filter</module>
|
||||||
<module>servlet-oauth-client</module>
|
<module>servlet-oauth-client</module>
|
||||||
<module>spring-boot</module>
|
<module>spring-boot</module>
|
||||||
|
<module>spring-boot-container-bundle</module>
|
||||||
<module>spring-security</module>
|
<module>spring-security</module>
|
||||||
<module>tomcat</module>
|
<module>tomcat</module>
|
||||||
<module>undertow</module>
|
<module>undertow</module>
|
||||||
<module>wildfly</module>
|
<module>wildfly</module>
|
||||||
|
<module>wildfly-elytron</module>
|
||||||
</modules>
|
</modules>
|
||||||
</project>
|
</project>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+58
-31
@@ -53,70 +53,97 @@ import java.util.regex.Pattern;
|
|||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class KeycloakOIDCFilter implements Filter {
|
public class KeycloakOIDCFilter implements Filter {
|
||||||
|
|
||||||
|
private final static Logger log = Logger.getLogger("" + KeycloakOIDCFilter.class);
|
||||||
|
|
||||||
|
public static final String SKIP_PATTERN_PARAM = "keycloak.config.skipPattern";
|
||||||
|
|
||||||
|
public static final String CONFIG_RESOLVER_PARAM = "keycloak.config.resolver";
|
||||||
|
|
||||||
|
public static final String CONFIG_FILE_PARAM = "keycloak.config.file";
|
||||||
|
|
||||||
|
public static final String CONFIG_PATH_PARAM = "keycloak.config.path";
|
||||||
|
|
||||||
protected AdapterDeploymentContext deploymentContext;
|
protected AdapterDeploymentContext deploymentContext;
|
||||||
|
|
||||||
protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
|
protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
|
||||||
|
|
||||||
protected NodesRegistrationManagement nodesRegistrationManagement;
|
protected NodesRegistrationManagement nodesRegistrationManagement;
|
||||||
|
|
||||||
protected Pattern skipPattern;
|
protected Pattern skipPattern;
|
||||||
|
|
||||||
private final static Logger log = Logger.getLogger(""+KeycloakOIDCFilter.class);
|
private final KeycloakConfigResolver definedconfigResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor that can be used to define a {@code KeycloakConfigResolver} that will be used at initialization to
|
||||||
|
* provide the {@code KeycloakDeployment}.
|
||||||
|
* @param definedconfigResolver the resolver
|
||||||
|
*/
|
||||||
|
public KeycloakOIDCFilter(KeycloakConfigResolver definedconfigResolver) {
|
||||||
|
this.definedconfigResolver = definedconfigResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeycloakOIDCFilter() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(final FilterConfig filterConfig) throws ServletException {
|
public void init(final FilterConfig filterConfig) throws ServletException {
|
||||||
|
String skipPatternDefinition = filterConfig.getInitParameter(SKIP_PATTERN_PARAM);
|
||||||
String skipPatternDefinition = filterConfig.getInitParameter("keycloak.config.skipPattern");
|
|
||||||
if (skipPatternDefinition != null) {
|
if (skipPatternDefinition != null) {
|
||||||
skipPattern = Pattern.compile(skipPatternDefinition, Pattern.DOTALL);
|
skipPattern = Pattern.compile(skipPatternDefinition, Pattern.DOTALL);
|
||||||
}
|
}
|
||||||
|
|
||||||
String configResolverClass = filterConfig.getInitParameter("keycloak.config.resolver");
|
if (definedconfigResolver != null) {
|
||||||
if (configResolverClass != null) {
|
deploymentContext = new AdapterDeploymentContext(definedconfigResolver);
|
||||||
try {
|
log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", definedconfigResolver.getClass());
|
||||||
KeycloakConfigResolver configResolver = (KeycloakConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance();
|
|
||||||
deploymentContext = new AdapterDeploymentContext(configResolver);
|
|
||||||
log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()});
|
|
||||||
deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
String fp = filterConfig.getInitParameter("keycloak.config.file");
|
String configResolverClass = filterConfig.getInitParameter(CONFIG_RESOLVER_PARAM);
|
||||||
InputStream is = null;
|
if (configResolverClass != null) {
|
||||||
if (fp != null) {
|
|
||||||
try {
|
try {
|
||||||
is = new FileInputStream(fp);
|
KeycloakConfigResolver configResolver = (KeycloakConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance();
|
||||||
} catch (FileNotFoundException e) {
|
deploymentContext = new AdapterDeploymentContext(configResolver);
|
||||||
throw new RuntimeException(e);
|
log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()});
|
||||||
|
deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String path = "/WEB-INF/keycloak.json";
|
String fp = filterConfig.getInitParameter(CONFIG_FILE_PARAM);
|
||||||
String pathParam = filterConfig.getInitParameter("keycloak.config.path");
|
InputStream is = null;
|
||||||
if (pathParam != null) path = pathParam;
|
if (fp != null) {
|
||||||
is = filterConfig.getServletContext().getResourceAsStream(path);
|
try {
|
||||||
|
is = new FileInputStream(fp);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String path = "/WEB-INF/keycloak.json";
|
||||||
|
String pathParam = filterConfig.getInitParameter(CONFIG_PATH_PARAM);
|
||||||
|
if (pathParam != null) path = pathParam;
|
||||||
|
is = filterConfig.getServletContext().getResourceAsStream(path);
|
||||||
|
}
|
||||||
|
KeycloakDeployment kd = createKeycloakDeploymentFrom(is);
|
||||||
|
deploymentContext = new AdapterDeploymentContext(kd);
|
||||||
|
log.fine("Keycloak is using a per-deployment configuration.");
|
||||||
}
|
}
|
||||||
KeycloakDeployment kd = createKeycloakDeploymentFrom(is);
|
|
||||||
deploymentContext = new AdapterDeploymentContext(kd);
|
|
||||||
log.fine("Keycloak is using a per-deployment configuration.");
|
|
||||||
}
|
}
|
||||||
filterConfig.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
|
filterConfig.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
|
||||||
nodesRegistrationManagement = new NodesRegistrationManagement();
|
nodesRegistrationManagement = new NodesRegistrationManagement();
|
||||||
}
|
}
|
||||||
|
|
||||||
private KeycloakDeployment createKeycloakDeploymentFrom(InputStream is) {
|
private KeycloakDeployment createKeycloakDeploymentFrom(InputStream is) {
|
||||||
|
|
||||||
if (is == null) {
|
if (is == null) {
|
||||||
log.fine("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
|
log.fine("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
|
||||||
return new KeycloakDeployment();
|
return new KeycloakDeployment();
|
||||||
}
|
}
|
||||||
|
|
||||||
return KeycloakDeploymentBuilder.build(is);
|
return KeycloakDeploymentBuilder.build(is);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
|
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
|
||||||
|
|
||||||
log.fine("Keycloak OIDC Filter");
|
log.fine("Keycloak OIDC Filter");
|
||||||
//System.err.println("Keycloak OIDC Filter: " + ((HttpServletRequest)req).getRequestURL().toString());
|
|
||||||
HttpServletRequest request = (HttpServletRequest) req;
|
HttpServletRequest request = (HttpServletRequest) req;
|
||||||
HttpServletResponse response = (HttpServletResponse) res;
|
HttpServletResponse response = (HttpServletResponse) res;
|
||||||
|
|
||||||
@@ -198,7 +225,7 @@ public class KeycloakOIDCFilter implements Filter {
|
|||||||
*
|
*
|
||||||
* @param request the request to check
|
* @param request the request to check
|
||||||
* @return {@code true} if the request should not be handled,
|
* @return {@code true} if the request should not be handled,
|
||||||
* {@code false} otherwise.
|
* {@code false} otherwise.
|
||||||
*/
|
*/
|
||||||
private boolean shouldSkip(HttpServletRequest request) {
|
private boolean shouldSkip(HttpServletRequest request) {
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -168,7 +168,7 @@ public class OIDCFilterSessionStore extends FilterSessionStore implements Adapte
|
|||||||
HttpSession httpSession = request.getSession();
|
HttpSession httpSession = request.getSession();
|
||||||
httpSession.setAttribute(KeycloakAccount.class.getName(), sAccount);
|
httpSession.setAttribute(KeycloakAccount.class.getName(), sAccount);
|
||||||
httpSession.setAttribute(KeycloakSecurityContext.class.getName(), sAccount.getKeycloakSecurityContext());
|
httpSession.setAttribute(KeycloakSecurityContext.class.getName(), sAccount.getKeycloakSecurityContext());
|
||||||
if (idMapper != null) idMapper.map(account.getKeycloakSecurityContext().getToken().getClientSession(), account.getPrincipal().getName(), httpSession.getId());
|
if (idMapper != null) idMapper.map(account.getKeycloakSecurityContext().getToken().getSessionState(), account.getPrincipal().getName(), httpSession.getId());
|
||||||
//String username = securityContext.getToken().getSubject();
|
//String username = securityContext.getToken().getSubject();
|
||||||
//log.fine("userSessionManagement.login: " + username);
|
//log.fine("userSessionManagement.login: " + username);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+68
-1
@@ -41,12 +41,60 @@ import java.io.InputStream;
|
|||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc7636#section-4
|
||||||
|
private String codeVerifier;
|
||||||
|
private String codeChallenge;
|
||||||
|
private String codeChallengeMethod = OAuth2Constants.PKCE_METHOD_S256;
|
||||||
|
private static Logger logger = Logger.getLogger(ServletOAuthClient.class);
|
||||||
|
|
||||||
|
public static String generateSecret() {
|
||||||
|
return generateSecret(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String generateSecret(int bytes) {
|
||||||
|
byte[] buf = new byte[bytes];
|
||||||
|
new SecureRandom().nextBytes(buf);
|
||||||
|
return Base64Url.encode(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCodeVerifier() {
|
||||||
|
codeVerifier = generateSecret();
|
||||||
|
logger.debugf("Generated codeVerifier = %s", codeVerifier);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCodeChallenge() {
|
||||||
|
try {
|
||||||
|
if (codeChallengeMethod.equals(OAuth2Constants.PKCE_METHOD_S256)) {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
md.update(codeVerifier.getBytes());
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : md.digest()) {
|
||||||
|
String hex = String.format("%02x", b);
|
||||||
|
sb.append(hex);
|
||||||
|
}
|
||||||
|
codeChallenge = Base64Url.encode(sb.toString().getBytes());
|
||||||
|
} else {
|
||||||
|
codeChallenge = Base64Url.encode(codeVerifier.getBytes());
|
||||||
|
}
|
||||||
|
logger.debugf("Encode codeChallenge = %s, codeChallengeMethod = %s", codeChallenge, codeChallengeMethod);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.info("PKCE client side unknown hash algorithm");
|
||||||
|
codeChallenge = Base64Url.encode(codeVerifier.getBytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* closes client
|
* closes client
|
||||||
*/
|
*/
|
||||||
@@ -57,7 +105,15 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
|||||||
private AccessTokenResponse resolveBearerToken(HttpServletRequest request, String redirectUri, String code) throws IOException, ServerRequest.HttpFailure {
|
private AccessTokenResponse resolveBearerToken(HttpServletRequest request, String redirectUri, String code) throws IOException, ServerRequest.HttpFailure {
|
||||||
// Don't send sessionId in oauth clients for now
|
// Don't send sessionId in oauth clients for now
|
||||||
KeycloakDeployment resolvedDeployment = resolveDeployment(getDeployment(), request);
|
KeycloakDeployment resolvedDeployment = resolveDeployment(getDeployment(), request);
|
||||||
return ServerRequest.invokeAccessCodeToToken(resolvedDeployment, code, redirectUri, null);
|
|
||||||
|
// https://tools.ietf.org/html/rfc7636#section-4
|
||||||
|
if (codeVerifier != null) {
|
||||||
|
logger.debugf("Before sending Token Request, codeVerifier = %s", codeVerifier);
|
||||||
|
return ServerRequest.invokeAccessCodeToToken(resolvedDeployment, code, redirectUri, null, codeVerifier);
|
||||||
|
} else {
|
||||||
|
logger.debug("Before sending Token Request without codeVerifier");
|
||||||
|
return ServerRequest.invokeAccessCodeToToken(resolvedDeployment, code, redirectUri, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,6 +150,12 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
|||||||
String authUrl = resolvedDeployment.getAuthUrl().clone().build().toString();
|
String authUrl = resolvedDeployment.getAuthUrl().clone().build().toString();
|
||||||
String scopeParam = TokenUtil.attachOIDCScope(scope);
|
String scopeParam = TokenUtil.attachOIDCScope(scope);
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc7636#section-4
|
||||||
|
if (resolvedDeployment.isPkce()) {
|
||||||
|
setCodeVerifier();
|
||||||
|
setCodeChallenge();
|
||||||
|
}
|
||||||
|
|
||||||
KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(authUrl)
|
KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(authUrl)
|
||||||
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
||||||
.queryParam(OAuth2Constants.CLIENT_ID, getClientId())
|
.queryParam(OAuth2Constants.CLIENT_ID, getClientId())
|
||||||
@@ -218,6 +280,11 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
|||||||
return servletRequest.getRequestURL().toString();
|
return servletRequest.getRequestURL().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRelativePath() {
|
||||||
|
return servletRequest.getServletPath();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSecure() {
|
public boolean isSecure() {
|
||||||
return servletRequest.isSecure();
|
return servletRequest.isSecure();
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-parent</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>3.2.0.Final</version>
|
||||||
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<artifactId>spring-boot-container-bundle</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-tomcat8-adapter</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-undertow-adapter</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-jetty93-adapter</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>2.4.3</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<artifactSet>
|
||||||
|
<includes>
|
||||||
|
<include>org.keycloak:keycloak-tomcat8-adapter</include>
|
||||||
|
<include>org.keycloak:keycloak-undertow-adapter</include>
|
||||||
|
<include>org.keycloak:keycloak-jetty93-adapter</include>
|
||||||
|
<include>org.keycloak:keycloak-tomcat-core-adapter</include>
|
||||||
|
<include>org.keycloak:keycloak-tomcat-adapter-spi</include>
|
||||||
|
<include>org.keycloak:keycloak-undertow-adapter</include>
|
||||||
|
<include>org.keycloak:keycloak-undertow-adapter-spi</include>
|
||||||
|
<include>org.keycloak:keycloak-jetty-core</include>
|
||||||
|
<include>org.keycloak:keycloak-jetty-adapter-spi</include>
|
||||||
|
</includes>
|
||||||
|
</artifactSet>
|
||||||
|
<createSourcesJar>true</createSourcesJar>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<description/>
|
<description/>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<spring-boot.version>1.2.1.RELEASE</spring-boot.version>
|
<spring-boot.version>1.3.0.RELEASE</spring-boot.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -43,74 +43,67 @@
|
|||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-core</artifactId>
|
<artifactId>keycloak-core</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>spring-boot-container-bundle</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<optional>true</optional>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-spring-security-adapter</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.keycloak</groupId>
|
|
||||||
<artifactId>keycloak-tomcat8-adapter</artifactId>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.keycloak</groupId>
|
|
||||||
<artifactId>keycloak-undertow-adapter</artifactId>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.keycloak</groupId>
|
|
||||||
<artifactId>keycloak-jetty92-adapter</artifactId>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
<version>${spring-boot.version}</version>
|
<version>${spring-boot.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.undertow</groupId>
|
||||||
|
<artifactId>undertow-servlet</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.eclipse.jetty</groupId>
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
<artifactId>jetty-server</artifactId>
|
<artifactId>jetty-server</artifactId>
|
||||||
<version>${jetty9.version}</version>
|
<version>${jetty9.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.eclipse.jetty</groupId>
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
<artifactId>jetty-security</artifactId>
|
<artifactId>jetty-security</artifactId>
|
||||||
<version>${jetty9.version}</version>
|
<version>${jetty9.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.eclipse.jetty</groupId>
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
<artifactId>jetty-webapp</artifactId>
|
<artifactId>jetty-webapp</artifactId>
|
||||||
<version>${jetty9.version}</version>
|
<version>${jetty9.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.undertow</groupId>
|
|
||||||
<artifactId>undertow-servlet</artifactId>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.undertow</groupId>
|
|
||||||
<artifactId>undertow-core</artifactId>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.keycloak</groupId>
|
|
||||||
<artifactId>keycloak-undertow-adapter-spi</artifactId>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>junit</groupId>
|
<groupId>junit</groupId>
|
||||||
<artifactId>junit</artifactId>
|
<artifactId>junit</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
@@ -118,11 +111,10 @@
|
|||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<configuration>
|
<configuration>
|
||||||
<source>1.6</source>
|
<source>1.7</source>
|
||||||
<target>1.6</target>
|
<target>1.7</target>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
+67
-37
@@ -33,6 +33,7 @@ import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
|
|||||||
import org.keycloak.adapters.undertow.KeycloakServletExtension;
|
import org.keycloak.adapters.undertow.KeycloakServletExtension;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||||
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
|
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
|
||||||
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
|
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
|
||||||
@@ -60,7 +61,8 @@ import java.util.Set;
|
|||||||
@Configuration
|
@Configuration
|
||||||
@ConditionalOnWebApplication
|
@ConditionalOnWebApplication
|
||||||
@EnableConfigurationProperties(KeycloakSpringBootProperties.class)
|
@EnableConfigurationProperties(KeycloakSpringBootProperties.class)
|
||||||
public class KeycloakSpringBootConfiguration {
|
@ConditionalOnProperty(value = "keycloak.enabled", matchIfMissing = true)
|
||||||
|
public class KeycloakAutoConfiguration {
|
||||||
|
|
||||||
private KeycloakSpringBootProperties keycloakProperties;
|
private KeycloakSpringBootProperties keycloakProperties;
|
||||||
|
|
||||||
@@ -70,6 +72,7 @@ public class KeycloakSpringBootConfiguration {
|
|||||||
KeycloakSpringBootConfigResolver.setAdapterConfig(keycloakProperties);
|
KeycloakSpringBootConfigResolver.setAdapterConfig(keycloakProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public EmbeddedServletContainerCustomizer getKeycloakContainerCustomizer() {
|
public EmbeddedServletContainerCustomizer getKeycloakContainerCustomizer() {
|
||||||
return new EmbeddedServletContainerCustomizer() {
|
return new EmbeddedServletContainerCustomizer() {
|
||||||
@@ -141,10 +144,10 @@ public class KeycloakSpringBootConfiguration {
|
|||||||
List<io.undertow.servlet.api.SecurityConstraint> undertowSecurityConstraints = new ArrayList<io.undertow.servlet.api.SecurityConstraint>();
|
List<io.undertow.servlet.api.SecurityConstraint> undertowSecurityConstraints = new ArrayList<io.undertow.servlet.api.SecurityConstraint>();
|
||||||
for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) {
|
for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) {
|
||||||
|
|
||||||
for (KeycloakSpringBootProperties.SecurityCollection collectionDefinition : constraintDefinition.getSecurityCollections()) {
|
io.undertow.servlet.api.SecurityConstraint undertowSecurityConstraint = new io.undertow.servlet.api.SecurityConstraint();
|
||||||
|
undertowSecurityConstraint.addRolesAllowed(constraintDefinition.getAuthRoles());
|
||||||
|
|
||||||
io.undertow.servlet.api.SecurityConstraint undertowSecurityConstraint = new io.undertow.servlet.api.SecurityConstraint();
|
for (KeycloakSpringBootProperties.SecurityCollection collectionDefinition : constraintDefinition.getSecurityCollections()) {
|
||||||
undertowSecurityConstraint.addRolesAllowed(collectionDefinition.getAuthRoles());
|
|
||||||
|
|
||||||
WebResourceCollection webResourceCollection = new WebResourceCollection();
|
WebResourceCollection webResourceCollection = new WebResourceCollection();
|
||||||
webResourceCollection.addHttpMethods(collectionDefinition.getMethods());
|
webResourceCollection.addHttpMethods(collectionDefinition.getMethods());
|
||||||
@@ -153,8 +156,9 @@ public class KeycloakSpringBootConfiguration {
|
|||||||
|
|
||||||
undertowSecurityConstraint.addWebResourceCollections(webResourceCollection);
|
undertowSecurityConstraint.addWebResourceCollections(webResourceCollection);
|
||||||
|
|
||||||
undertowSecurityConstraints.add(undertowSecurityConstraint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
undertowSecurityConstraints.add(undertowSecurityConstraint);
|
||||||
}
|
}
|
||||||
return undertowSecurityConstraints;
|
return undertowSecurityConstraints;
|
||||||
}
|
}
|
||||||
@@ -174,42 +178,70 @@ public class KeycloakSpringBootConfiguration {
|
|||||||
KeycloakJettyAuthenticator keycloakJettyAuthenticator = new KeycloakJettyAuthenticator();
|
KeycloakJettyAuthenticator keycloakJettyAuthenticator = new KeycloakJettyAuthenticator();
|
||||||
keycloakJettyAuthenticator.setConfigResolver(new KeycloakSpringBootConfigResolver());
|
keycloakJettyAuthenticator.setConfigResolver(new KeycloakSpringBootConfigResolver());
|
||||||
|
|
||||||
|
/* see org.eclipse.jetty.webapp.StandardDescriptorProcessor#visitSecurityConstraint for an example
|
||||||
|
on how to map servlet spec to Constraints */
|
||||||
|
|
||||||
List<ConstraintMapping> jettyConstraintMappings = new ArrayList<ConstraintMapping>();
|
List<ConstraintMapping> jettyConstraintMappings = new ArrayList<ConstraintMapping>();
|
||||||
for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) {
|
for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) {
|
||||||
|
|
||||||
for (KeycloakSpringBootProperties.SecurityCollection securityCollectionDefinition : constraintDefinition
|
for (KeycloakSpringBootProperties.SecurityCollection securityCollectionDefinition : constraintDefinition
|
||||||
.getSecurityCollections()) {
|
.getSecurityCollections()) {
|
||||||
|
// securityCollection matches servlet spec's web-resource-collection
|
||||||
Constraint jettyConstraint = new Constraint();
|
Constraint jettyConstraint = new Constraint();
|
||||||
|
|
||||||
|
if (constraintDefinition.getAuthRoles().size() > 0) {
|
||||||
|
jettyConstraint.setAuthenticate(true);
|
||||||
|
jettyConstraint.setRoles(constraintDefinition.getAuthRoles().toArray(new String[0]));
|
||||||
|
}
|
||||||
|
|
||||||
jettyConstraint.setName(securityCollectionDefinition.getName());
|
jettyConstraint.setName(securityCollectionDefinition.getName());
|
||||||
jettyConstraint.setAuthenticate(true);
|
|
||||||
|
|
||||||
if (securityCollectionDefinition.getName() != null) {
|
// according to the servlet spec each security-constraint has at least one URL pattern
|
||||||
jettyConstraint.setName(securityCollectionDefinition.getName());
|
for(String pattern : securityCollectionDefinition.getPatterns()) {
|
||||||
|
|
||||||
|
/* the following code is asymmetric as Jetty's ConstraintMapping accepts only one allowed HTTP method,
|
||||||
|
but multiple omitted methods. Therefore we add one ConstraintMapping for each allowed
|
||||||
|
mapping but only one mapping in the cases of omitted methods or no methods.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (securityCollectionDefinition.getMethods().size() > 0) {
|
||||||
|
// according to the servlet spec we have either methods ...
|
||||||
|
for(String method : securityCollectionDefinition.getMethods()) {
|
||||||
|
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
||||||
|
jettyConstraintMappings.add(jettyConstraintMapping);
|
||||||
|
|
||||||
|
jettyConstraintMapping.setConstraint(jettyConstraint);
|
||||||
|
jettyConstraintMapping.setPathSpec(pattern);
|
||||||
|
jettyConstraintMapping.setMethod(method);
|
||||||
|
}
|
||||||
|
} else if (securityCollectionDefinition.getOmittedMethods().size() > 0){
|
||||||
|
// ... omitted methods ...
|
||||||
|
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
||||||
|
jettyConstraintMappings.add(jettyConstraintMapping);
|
||||||
|
|
||||||
|
jettyConstraintMapping.setConstraint(jettyConstraint);
|
||||||
|
jettyConstraintMapping.setPathSpec(pattern);
|
||||||
|
jettyConstraintMapping.setMethodOmissions(
|
||||||
|
securityCollectionDefinition.getOmittedMethods().toArray(new String[0]));
|
||||||
|
} else {
|
||||||
|
// ... or no methods at all
|
||||||
|
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
||||||
|
jettyConstraintMappings.add(jettyConstraintMapping);
|
||||||
|
|
||||||
|
jettyConstraintMapping.setConstraint(jettyConstraint);
|
||||||
|
jettyConstraintMapping.setPathSpec(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
jettyConstraint.setRoles(securityCollectionDefinition.getAuthRoles().toArray(new String[0]));
|
|
||||||
|
|
||||||
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
|
||||||
if (securityCollectionDefinition.getPatterns().size() > 0) {
|
|
||||||
//First pattern wins
|
|
||||||
jettyConstraintMapping.setPathSpec(securityCollectionDefinition.getPatterns().get(0));
|
|
||||||
jettyConstraintMapping.setConstraint(jettyConstraint);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (securityCollectionDefinition.getMethods().size() > 0) {
|
|
||||||
//First method wins
|
|
||||||
jettyConstraintMapping.setMethod(securityCollectionDefinition.getMethods().get(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
jettyConstraintMapping.setMethodOmissions(
|
|
||||||
securityCollectionDefinition.getOmittedMethods().toArray(new String[0]));
|
|
||||||
|
|
||||||
jettyConstraintMappings.add(jettyConstraintMapping);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WebAppContext webAppContext = server.getBean(WebAppContext.class);
|
WebAppContext webAppContext = server.getBean(WebAppContext.class);
|
||||||
|
//if not found as registered bean let's try the handler
|
||||||
|
if(webAppContext==null){
|
||||||
|
webAppContext = (WebAppContext) server.getHandler();
|
||||||
|
}
|
||||||
|
|
||||||
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
|
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
|
||||||
securityHandler.setConstraintMappings(jettyConstraintMappings);
|
securityHandler.setConstraintMappings(jettyConstraintMappings);
|
||||||
@@ -235,12 +267,10 @@ public class KeycloakSpringBootConfiguration {
|
|||||||
|
|
||||||
Set<String> authRoles = new HashSet<String>();
|
Set<String> authRoles = new HashSet<String>();
|
||||||
for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) {
|
for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) {
|
||||||
for (KeycloakSpringBootProperties.SecurityCollection collection : constraint.getSecurityCollections()) {
|
for (String authRole : constraint.getAuthRoles()) {
|
||||||
for (String authRole : collection.getAuthRoles()) {
|
if (!authRoles.contains(authRole)) {
|
||||||
if (!authRoles.contains(authRole)) {
|
context.addSecurityRole(authRole);
|
||||||
context.addSecurityRole(authRole);
|
authRoles.add(authRole);
|
||||||
authRoles.add(authRole);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,6 +278,10 @@ public class KeycloakSpringBootConfiguration {
|
|||||||
for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) {
|
for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) {
|
||||||
SecurityConstraint tomcatConstraint = new SecurityConstraint();
|
SecurityConstraint tomcatConstraint = new SecurityConstraint();
|
||||||
|
|
||||||
|
for (String authRole : constraint.getAuthRoles()) {
|
||||||
|
tomcatConstraint.addAuthRole(authRole);
|
||||||
|
}
|
||||||
|
|
||||||
for (KeycloakSpringBootProperties.SecurityCollection collection : constraint.getSecurityCollections()) {
|
for (KeycloakSpringBootProperties.SecurityCollection collection : constraint.getSecurityCollections()) {
|
||||||
SecurityCollection tomcatSecCollection = new SecurityCollection();
|
SecurityCollection tomcatSecCollection = new SecurityCollection();
|
||||||
|
|
||||||
@@ -258,10 +292,6 @@ public class KeycloakSpringBootConfiguration {
|
|||||||
tomcatSecCollection.setDescription(collection.getDescription());
|
tomcatSecCollection.setDescription(collection.getDescription());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String authRole : collection.getAuthRoles()) {
|
|
||||||
tomcatConstraint.addAuthRole(authRole);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String pattern : collection.getPatterns()) {
|
for (String pattern : collection.getPatterns()) {
|
||||||
tomcatSecCollection.addPattern(pattern);
|
tomcatSecCollection.addPattern(pattern);
|
||||||
}
|
}
|
||||||
+52
-9
@@ -34,14 +34,42 @@ public class KeycloakSpringBootProperties extends AdapterConfig {
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private Map config = new HashMap();
|
private Map config = new HashMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow enabling of Keycloak Spring Boot adapter by configuration.
|
||||||
|
*/
|
||||||
|
private boolean enabled = true;
|
||||||
|
|
||||||
public Map getConfig() {
|
public Map getConfig() {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To provide Java EE security constraints
|
||||||
|
*/
|
||||||
private List<SecurityConstraint> securityConstraints = new ArrayList<SecurityConstraint>();
|
private List<SecurityConstraint> securityConstraints = new ArrayList<SecurityConstraint>();
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This matches security-constraint of the servlet spec
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties()
|
||||||
public static class SecurityConstraint {
|
public static class SecurityConstraint {
|
||||||
|
/**
|
||||||
|
* A list of security collections
|
||||||
|
*/
|
||||||
private List<SecurityCollection> securityCollections = new ArrayList<SecurityCollection>();
|
private List<SecurityCollection> securityCollections = new ArrayList<SecurityCollection>();
|
||||||
|
private List<String> authRoles = new ArrayList<String>();
|
||||||
|
|
||||||
|
public List<String> getAuthRoles() {
|
||||||
|
return authRoles;
|
||||||
|
}
|
||||||
|
|
||||||
public List<SecurityCollection> getSecurityCollections() {
|
public List<SecurityCollection> getSecurityCollections() {
|
||||||
return securityCollections;
|
return securityCollections;
|
||||||
@@ -50,20 +78,39 @@ public class KeycloakSpringBootProperties extends AdapterConfig {
|
|||||||
public void setSecurityCollections(List<SecurityCollection> securityCollections) {
|
public void setSecurityCollections(List<SecurityCollection> securityCollections) {
|
||||||
this.securityCollections = securityCollections;
|
this.securityCollections = securityCollections;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setAuthRoles(List<String> authRoles) {
|
||||||
|
this.authRoles = authRoles;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This matches web-resource-collection of the servlet spec
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties()
|
||||||
public static class SecurityCollection {
|
public static class SecurityCollection {
|
||||||
|
/**
|
||||||
|
* The name of your security constraint
|
||||||
|
*/
|
||||||
private String name;
|
private String name;
|
||||||
|
/**
|
||||||
|
* The description of your security collection
|
||||||
|
*/
|
||||||
private String description;
|
private String description;
|
||||||
private List<String> authRoles = new ArrayList<String>();
|
/**
|
||||||
|
* A list of URL patterns that should match to apply the security collection
|
||||||
|
*/
|
||||||
private List<String> patterns = new ArrayList<String>();
|
private List<String> patterns = new ArrayList<String>();
|
||||||
|
/**
|
||||||
|
* A list of HTTP methods that applies for this security collection
|
||||||
|
*/
|
||||||
private List<String> methods = new ArrayList<String>();
|
private List<String> methods = new ArrayList<String>();
|
||||||
|
/**
|
||||||
|
* A list of HTTP methods that will be omitted for this security collection
|
||||||
|
*/
|
||||||
private List<String> omittedMethods = new ArrayList<String>();
|
private List<String> omittedMethods = new ArrayList<String>();
|
||||||
|
|
||||||
public List<String> getAuthRoles() {
|
|
||||||
return authRoles;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getPatterns() {
|
public List<String> getPatterns() {
|
||||||
return patterns;
|
return patterns;
|
||||||
}
|
}
|
||||||
@@ -92,10 +139,6 @@ public class KeycloakSpringBootProperties extends AdapterConfig {
|
|||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAuthRoles(List<String> authRoles) {
|
|
||||||
this.authRoles = authRoles;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPatterns(List<String> patterns) {
|
public void setPatterns(List<String> patterns) {
|
||||||
this.patterns = patterns;
|
this.patterns = patterns;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||||
org.keycloak.adapters.springboot.KeycloakSpringBootConfiguration
|
org.keycloak.adapters.springboot.KeycloakAutoConfiguration
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
@@ -59,12 +59,14 @@
|
|||||||
<groupId>org.springframework.security</groupId>
|
<groupId>org.springframework.security</groupId>
|
||||||
<artifactId>spring-security-config</artifactId>
|
<artifactId>spring-security-config</artifactId>
|
||||||
<version>${spring-security.version}</version>
|
<version>${spring-security.version}</version>
|
||||||
|
<optional>true</optional>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.security</groupId>
|
<groupId>org.springframework.security</groupId>
|
||||||
<artifactId>spring-security-web</artifactId>
|
<artifactId>spring-security-web</artifactId>
|
||||||
<version>${spring-security.version}</version>
|
<version>${spring-security.version}</version>
|
||||||
|
<optional>true</optional>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package org.keycloak.adapters.springsecurity;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import static java.lang.annotation.ElementType.TYPE;
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add this annotation to a class that extends {@code KeycloakWebSecurityConfigurerAdapter} to provide
|
||||||
|
* a keycloak based Spring security configuration.
|
||||||
|
*
|
||||||
|
* @author Hendrik Ebbers
|
||||||
|
*/
|
||||||
|
@Retention(value = RUNTIME)
|
||||||
|
@Target(value = { TYPE })
|
||||||
|
@Configuration
|
||||||
|
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
|
||||||
|
@EnableWebSecurity
|
||||||
|
public @interface KeycloakConfiguration {
|
||||||
|
}
|
||||||
+12
-2
@@ -30,6 +30,12 @@ import javax.servlet.ServletException;
|
|||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
|
||||||
|
import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a Keycloak {@link AuthenticationEntryPoint authentication entry point}. Uses a
|
* Provides a Keycloak {@link AuthenticationEntryPoint authentication entry point}. Uses a
|
||||||
@@ -56,12 +62,15 @@ public class KeycloakAuthenticationEntryPoint implements AuthenticationEntryPoin
|
|||||||
private final RequestMatcher apiRequestMatcher;
|
private final RequestMatcher apiRequestMatcher;
|
||||||
private String loginUri = DEFAULT_LOGIN_URI;
|
private String loginUri = DEFAULT_LOGIN_URI;
|
||||||
private String realm = DEFAULT_REALM;
|
private String realm = DEFAULT_REALM;
|
||||||
|
|
||||||
|
private AdapterDeploymentContext adapterDeploymentContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Keycloak authentication entry point.
|
* Creates a new Keycloak authentication entry point.
|
||||||
*/
|
*/
|
||||||
public KeycloakAuthenticationEntryPoint() {
|
public KeycloakAuthenticationEntryPoint(AdapterDeploymentContext adapterDeploymentContext) {
|
||||||
this(DEFAULT_API_REQUEST_MATCHER);
|
this(DEFAULT_API_REQUEST_MATCHER);
|
||||||
|
this.adapterDeploymentContext = adapterDeploymentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,7 +88,8 @@ public class KeycloakAuthenticationEntryPoint implements AuthenticationEntryPoin
|
|||||||
@Override
|
@Override
|
||||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException
|
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException
|
||||||
{
|
{
|
||||||
if (apiRequestMatcher.matches(request)) {
|
HttpFacade facade = new SimpleHttpFacade(request, response);
|
||||||
|
if (apiRequestMatcher.matches(request) || adapterDeploymentContext.resolveDeployment(facade).isBearerOnly()) {
|
||||||
commenceUnauthorizedResponse(request, response);
|
commenceUnauthorizedResponse(request, response);
|
||||||
} else {
|
} else {
|
||||||
commenceLoginRedirect(request, response);
|
commenceLoginRedirect(request, response);
|
||||||
|
|||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.springsecurity.authentication;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To return the forbidden code with the corresponding message.
|
||||||
|
*
|
||||||
|
* @author emilienbondu
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class KeycloakAuthenticationFailureHandler implements AuthenticationFailureHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
|
||||||
|
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unable to authenticate using the Authorization header");
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -71,8 +71,8 @@ public abstract class KeycloakWebSecurityConfigurerAdapter extends WebSecurityCo
|
|||||||
return factoryBean.getObject();
|
return factoryBean.getObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected AuthenticationEntryPoint authenticationEntryPoint() {
|
protected AuthenticationEntryPoint authenticationEntryPoint() throws Exception {
|
||||||
return new KeycloakAuthenticationEntryPoint();
|
return new KeycloakAuthenticationEntryPoint(adapterDeploymentContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() {
|
protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() {
|
||||||
|
|||||||
+5
@@ -70,6 +70,11 @@ class WrappedHttpServletRequest implements Request {
|
|||||||
return buf.toString();
|
return buf.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRelativePath() {
|
||||||
|
return request.getServletPath();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSecure() {
|
public boolean isSecure() {
|
||||||
return request.isSecure();
|
return request.isSecure();
|
||||||
|
|||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.springsecurity.filter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.ServletRequest;
|
||||||
|
import javax.servlet.ServletResponse;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.AuthenticatedActionsHandler;
|
||||||
|
import org.keycloak.adapters.NodesRegistrationManagement;
|
||||||
|
import org.keycloak.adapters.OIDCHttpFacade;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
import org.springframework.web.filter.GenericFilterBean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes a Keycloak adapter {@link AuthenticatedActionsHandler} as a Spring Security filter.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class KeycloakAuthenticatedActionsFilter extends GenericFilterBean implements ApplicationContextAware {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(KeycloakAuthenticatedActionsFilter.class);
|
||||||
|
|
||||||
|
private final NodesRegistrationManagement management = new NodesRegistrationManagement();
|
||||||
|
private ApplicationContext applicationContext;
|
||||||
|
private AdapterDeploymentContext deploymentContext;
|
||||||
|
|
||||||
|
|
||||||
|
public KeycloakAuthenticatedActionsFilter() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initFilterBean() throws ServletException {
|
||||||
|
deploymentContext = applicationContext.getBean(AdapterDeploymentContext.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
log.debug("Unregistering deployment");
|
||||||
|
management.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
HttpFacade facade = new SimpleHttpFacade((HttpServletRequest)request, (HttpServletResponse)response);
|
||||||
|
AuthenticatedActionsHandler handler = new AuthenticatedActionsHandler(deploymentContext.resolveDeployment(facade), (OIDCHttpFacade)facade);
|
||||||
|
boolean handled = handler.handledRequest();
|
||||||
|
if (handled) {
|
||||||
|
log.debug("Authenticated filter handled request: {}", ((HttpServletRequest) request).getRequestURI());
|
||||||
|
} else {
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
-26
@@ -17,6 +17,13 @@
|
|||||||
|
|
||||||
package org.keycloak.adapters.springsecurity.filter;
|
package org.keycloak.adapters.springsecurity.filter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.keycloak.adapters.AdapterDeploymentContext;
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
import org.keycloak.adapters.AdapterTokenStore;
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
@@ -25,7 +32,7 @@ import org.keycloak.adapters.spi.AuthChallenge;
|
|||||||
import org.keycloak.adapters.spi.AuthOutcome;
|
import org.keycloak.adapters.spi.AuthOutcome;
|
||||||
import org.keycloak.adapters.spi.HttpFacade;
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
import org.keycloak.adapters.springsecurity.KeycloakAuthenticationException;
|
import org.keycloak.adapters.springsecurity.KeycloakAuthenticationException;
|
||||||
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint;
|
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationFailureHandler;
|
||||||
import org.keycloak.adapters.springsecurity.authentication.SpringSecurityRequestAuthenticator;
|
import org.keycloak.adapters.springsecurity.authentication.SpringSecurityRequestAuthenticator;
|
||||||
import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade;
|
import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade;
|
||||||
import org.keycloak.adapters.springsecurity.token.AdapterTokenStoreFactory;
|
import org.keycloak.adapters.springsecurity.token.AdapterTokenStoreFactory;
|
||||||
@@ -41,19 +48,12 @@ import org.springframework.security.core.Authentication;
|
|||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||||
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
|
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
|
||||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
import javax.servlet.FilterChain;
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a Keycloak authentication processing filter.
|
* Provides a Keycloak authentication processing filter.
|
||||||
*
|
*
|
||||||
@@ -61,11 +61,13 @@ import java.io.IOException;
|
|||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter implements ApplicationContextAware {
|
public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter implements ApplicationContextAware {
|
||||||
|
|
||||||
public static final String DEFAULT_LOGIN_URL = "/sso/login";
|
public static final String DEFAULT_LOGIN_URL = "/sso/login";
|
||||||
public static final String AUTHORIZATION_HEADER = "Authorization";
|
public static final String AUTHORIZATION_HEADER = "Authorization";
|
||||||
public static final String SCHEME_BEARER = "bearer ";
|
public static final String SCHEME_BEARER = "bearer ";
|
||||||
public static final String SCHEME_BASIC = "basic ";
|
public static final String SCHEME_BASIC = "basic ";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request matcher that matches requests to the {@link KeycloakAuthenticationEntryPoint#DEFAULT_LOGIN_URI default login URI}
|
* Request matcher that matches requests to the {@link KeycloakAuthenticationEntryPoint#DEFAULT_LOGIN_URI default login URI}
|
||||||
* and any request with a <code>Authorization</code> header.
|
* and any request with a <code>Authorization</code> header.
|
||||||
@@ -89,7 +91,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
|
|||||||
*/
|
*/
|
||||||
public KeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager) {
|
public KeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager) {
|
||||||
this(authenticationManager, DEFAULT_REQUEST_MATCHER);
|
this(authenticationManager, DEFAULT_REQUEST_MATCHER);
|
||||||
setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(DEFAULT_LOGIN_URL));
|
setAuthenticationFailureHandler(new KeycloakAuthenticationFailureHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,7 +111,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public KeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager, RequestMatcher
|
public KeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager, RequestMatcher
|
||||||
requiresAuthenticationRequestMatcher) {
|
requiresAuthenticationRequestMatcher) {
|
||||||
super(requiresAuthenticationRequestMatcher);
|
super(requiresAuthenticationRequestMatcher);
|
||||||
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
|
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
|
||||||
this.authenticationManager = authenticationManager;
|
this.authenticationManager = authenticationManager;
|
||||||
@@ -140,8 +142,27 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
|
|||||||
log.debug("Auth outcome: {}", result);
|
log.debug("Auth outcome: {}", result);
|
||||||
|
|
||||||
if (AuthOutcome.FAILED.equals(result)) {
|
if (AuthOutcome.FAILED.equals(result)) {
|
||||||
throw new KeycloakAuthenticationException("Auth outcome: " + result);
|
AuthChallenge challenge = authenticator.getChallenge();
|
||||||
|
if (challenge != null) {
|
||||||
|
challenge.challenge(facade);
|
||||||
|
}
|
||||||
|
throw new KeycloakAuthenticationException("Invalid authorization header, see WWW-Authenticate header for details");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (AuthOutcome.NOT_ATTEMPTED.equals(result)) {
|
||||||
|
AuthChallenge challenge = authenticator.getChallenge();
|
||||||
|
if (challenge != null) {
|
||||||
|
challenge.challenge(facade);
|
||||||
|
}
|
||||||
|
if (deployment.isBearerOnly()) {
|
||||||
|
// no redirection in this mode, throwing exception for the spring handler
|
||||||
|
throw new KeycloakAuthenticationException("Authorization header not found, see WWW-Authenticate header");
|
||||||
|
} else {
|
||||||
|
// let continue if challenged, it may redirect
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else if (AuthOutcome.AUTHENTICATED.equals(result)) {
|
else if (AuthOutcome.AUTHENTICATED.equals(result)) {
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
Assert.notNull(authentication, "Authentication SecurityContextHolder was null");
|
Assert.notNull(authentication, "Authentication SecurityContextHolder was null");
|
||||||
@@ -183,7 +204,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
|
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
|
||||||
Authentication authResult) throws IOException, ServletException {
|
Authentication authResult) throws IOException, ServletException {
|
||||||
|
|
||||||
if (!(this.isBearerTokenRequest(request) || this.isBasicAuthRequest(request))) {
|
if (!(this.isBearerTokenRequest(request) || this.isBasicAuthRequest(request))) {
|
||||||
super.successfulAuthentication(request, response, chain, authResult);
|
super.successfulAuthentication(request, response, chain, authResult);
|
||||||
@@ -211,19 +232,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
|
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
|
||||||
AuthenticationException failed) throws IOException, ServletException {
|
AuthenticationException failed) throws IOException, ServletException {
|
||||||
|
|
||||||
if (this.isBearerTokenRequest(request)) {
|
|
||||||
SecurityContextHolder.clearContext();
|
|
||||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unable to authenticate bearer token");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (this.isBasicAuthRequest(request)) {
|
|
||||||
SecurityContextHolder.clearContext();
|
|
||||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unable to authenticate with basic authentication");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
super.unsuccessfulAuthentication(request, response, failed);
|
super.unsuccessfulAuthentication(request, response, failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,4 +270,4 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
|
|||||||
public final void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) {
|
public final void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) {
|
||||||
throw new UnsupportedOperationException("This filter does not support explicitly setting a continue chain before success policy");
|
throw new UnsupportedOperationException("This filter does not support explicitly setting a continue chain before success policy");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+22
-1
@@ -26,6 +26,15 @@ import org.springframework.mock.web.MockHttpServletResponse;
|
|||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import static org.mockito.Matchers.any;
|
||||||
|
import static org.mockito.Matchers.eq;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keycloak authentication entry point tests.
|
* Keycloak authentication entry point tests.
|
||||||
@@ -35,12 +44,24 @@ public class KeycloakAuthenticationEntryPointTest {
|
|||||||
private KeycloakAuthenticationEntryPoint authenticationEntryPoint;
|
private KeycloakAuthenticationEntryPoint authenticationEntryPoint;
|
||||||
private MockHttpServletRequest request;
|
private MockHttpServletRequest request;
|
||||||
private MockHttpServletResponse response;
|
private MockHttpServletResponse response;
|
||||||
|
@Mock
|
||||||
|
private ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private AdapterDeploymentContext adapterDeploymentContext;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private KeycloakDeployment keycloakDeployment;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
authenticationEntryPoint = new KeycloakAuthenticationEntryPoint();
|
MockitoAnnotations.initMocks(this);
|
||||||
|
authenticationEntryPoint = new KeycloakAuthenticationEntryPoint(adapterDeploymentContext);
|
||||||
request = new MockHttpServletRequest();
|
request = new MockHttpServletRequest();
|
||||||
response = new MockHttpServletResponse();
|
response = new MockHttpServletResponse();
|
||||||
|
when(applicationContext.getBean(eq(AdapterDeploymentContext.class))).thenReturn(adapterDeploymentContext);
|
||||||
|
when(adapterDeploymentContext.resolveDeployment(any(HttpFacade.class))).thenReturn(keycloakDeployment);
|
||||||
|
when(keycloakDeployment.isBearerOnly()).thenReturn(Boolean.FALSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+24
-5
@@ -27,6 +27,7 @@ import org.keycloak.adapters.OidcKeycloakAccount;
|
|||||||
import org.keycloak.adapters.spi.HttpFacade;
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
import org.keycloak.adapters.springsecurity.KeycloakAuthenticationException;
|
import org.keycloak.adapters.springsecurity.KeycloakAuthenticationException;
|
||||||
import org.keycloak.adapters.springsecurity.account.KeycloakRole;
|
import org.keycloak.adapters.springsecurity.account.KeycloakRole;
|
||||||
|
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationFailureHandler;
|
||||||
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
|
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
|
||||||
import org.keycloak.common.enums.SslRequired;
|
import org.keycloak.common.enums.SslRequired;
|
||||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||||
@@ -89,6 +90,8 @@ public class KeycloakAuthenticationProcessingFilterTest {
|
|||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private AuthenticationFailureHandler failureHandler;
|
private AuthenticationFailureHandler failureHandler;
|
||||||
|
|
||||||
|
private KeycloakAuthenticationFailureHandler keycloakFailureHandler;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private OidcKeycloakAccount keycloakAccount;
|
private OidcKeycloakAccount keycloakAccount;
|
||||||
@@ -106,6 +109,7 @@ public class KeycloakAuthenticationProcessingFilterTest {
|
|||||||
MockitoAnnotations.initMocks(this);
|
MockitoAnnotations.initMocks(this);
|
||||||
request = spy(new MockHttpServletRequest());
|
request = spy(new MockHttpServletRequest());
|
||||||
filter = new KeycloakAuthenticationProcessingFilter(authenticationManager);
|
filter = new KeycloakAuthenticationProcessingFilter(authenticationManager);
|
||||||
|
keycloakFailureHandler = new KeycloakAuthenticationFailureHandler();
|
||||||
|
|
||||||
filter.setApplicationContext(applicationContext);
|
filter.setApplicationContext(applicationContext);
|
||||||
filter.setAuthenticationSuccessHandler(successHandler);
|
filter.setAuthenticationSuccessHandler(successHandler);
|
||||||
@@ -154,8 +158,9 @@ public class KeycloakAuthenticationProcessingFilterTest {
|
|||||||
when(keycloakDeployment.getResourceName()).thenReturn("resource-name");
|
when(keycloakDeployment.getResourceName()).thenReturn("resource-name");
|
||||||
when(keycloakDeployment.getStateCookieName()).thenReturn("kc-cookie");
|
when(keycloakDeployment.getStateCookieName()).thenReturn("kc-cookie");
|
||||||
when(keycloakDeployment.getSslRequired()).thenReturn(SslRequired.NONE);
|
when(keycloakDeployment.getSslRequired()).thenReturn(SslRequired.NONE);
|
||||||
filter.attemptAuthentication(request, response);
|
when(keycloakDeployment.isBearerOnly()).thenReturn(Boolean.FALSE);
|
||||||
|
|
||||||
|
filter.attemptAuthentication(request, response);
|
||||||
verify(response).setStatus(302);
|
verify(response).setStatus(302);
|
||||||
verify(response).setHeader(eq("Location"), startsWith("http://localhost:8080/auth"));
|
verify(response).setHeader(eq("Location"), startsWith("http://localhost:8080/auth"));
|
||||||
}
|
}
|
||||||
@@ -166,6 +171,13 @@ public class KeycloakAuthenticationProcessingFilterTest {
|
|||||||
filter.attemptAuthentication(request, response);
|
filter.attemptAuthentication(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(expected = KeycloakAuthenticationException.class)
|
||||||
|
public void testAttemptAuthenticationWithInvalidTokenBearerOnly() throws Exception {
|
||||||
|
when(keycloakDeployment.isBearerOnly()).thenReturn(Boolean.TRUE);
|
||||||
|
request.addHeader("Authorization", "Bearer xxx");
|
||||||
|
filter.attemptAuthentication(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSuccessfulAuthenticationInteractive() throws Exception {
|
public void testSuccessfulAuthenticationInteractive() throws Exception {
|
||||||
Authentication authentication = new KeycloakAuthenticationToken(keycloakAccount, authorities);
|
Authentication authentication = new KeycloakAuthenticationToken(keycloakAccount, authorities);
|
||||||
@@ -209,8 +221,7 @@ public class KeycloakAuthenticationProcessingFilterTest {
|
|||||||
AuthenticationException exception = new BadCredentialsException("OOPS");
|
AuthenticationException exception = new BadCredentialsException("OOPS");
|
||||||
this.setBearerAuthHeader(request);
|
this.setBearerAuthHeader(request);
|
||||||
filter.unsuccessfulAuthentication(request, response, exception);
|
filter.unsuccessfulAuthentication(request, response, exception);
|
||||||
verify(response).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), anyString());
|
verify(failureHandler).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class),
|
||||||
verify(failureHandler, never()).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class),
|
|
||||||
any(AuthenticationException.class));
|
any(AuthenticationException.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,10 +230,18 @@ public class KeycloakAuthenticationProcessingFilterTest {
|
|||||||
AuthenticationException exception = new BadCredentialsException("OOPS");
|
AuthenticationException exception = new BadCredentialsException("OOPS");
|
||||||
this.setBasicAuthHeader(request);
|
this.setBasicAuthHeader(request);
|
||||||
filter.unsuccessfulAuthentication(request, response, exception);
|
filter.unsuccessfulAuthentication(request, response, exception);
|
||||||
verify(response).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), anyString());
|
verify(failureHandler).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class),
|
||||||
verify(failureHandler, never()).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class),
|
|
||||||
any(AuthenticationException.class));
|
any(AuthenticationException.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDefaultFailureHanlder() throws Exception {
|
||||||
|
AuthenticationException exception = new BadCredentialsException("OOPS");
|
||||||
|
filter.setAuthenticationFailureHandler(keycloakFailureHandler);
|
||||||
|
filter.unsuccessfulAuthentication(request, response, exception);
|
||||||
|
|
||||||
|
verify(response).sendError(eq(HttpServletResponse.SC_UNAUTHORIZED), any(String.class));
|
||||||
|
}
|
||||||
|
|
||||||
@Test(expected = UnsupportedOperationException.class)
|
@Test(expected = UnsupportedOperationException.class)
|
||||||
public void testSetAllowSessionCreation() throws Exception {
|
public void testSetAllowSessionCreation() throws Exception {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<name>Keycloak Tomcat Integration</name>
|
<name>Keycloak Tomcat Integration</name>
|
||||||
@@ -32,8 +32,21 @@
|
|||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>tomcat-core</module>
|
<module>tomcat-core</module>
|
||||||
<module>tomcat6</module>
|
|
||||||
<module>tomcat7</module>
|
|
||||||
<module>tomcat8</module>
|
<module>tomcat8</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>community</id>
|
||||||
|
<activation>
|
||||||
|
<property>
|
||||||
|
<name>!product</name>
|
||||||
|
</property>
|
||||||
|
</activation>
|
||||||
|
<modules>
|
||||||
|
<module>tomcat6</module>
|
||||||
|
<module>tomcat7</module>
|
||||||
|
</modules>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
</project>
|
</project>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
@@ -29,8 +29,6 @@
|
|||||||
<artifactId>keycloak-tomcat-core-adapter</artifactId>
|
<artifactId>keycloak-tomcat-core-adapter</artifactId>
|
||||||
<name>Keycloak Tomcat Core Integration</name>
|
<name>Keycloak Tomcat Core Integration</name>
|
||||||
<properties>
|
<properties>
|
||||||
<!-- <tomcat.version>8.0.14</tomcat.version> -->
|
|
||||||
<!-- <tomcat.version>7.0.52</tomcat.version> -->
|
|
||||||
<tomcat.version>6.0.41</tomcat.version>
|
<tomcat.version>6.0.41</tomcat.version>
|
||||||
</properties>
|
</properties>
|
||||||
<description />
|
<description />
|
||||||
|
|||||||
+9
-11
@@ -25,6 +25,7 @@ import org.apache.catalina.Manager;
|
|||||||
import org.apache.catalina.authenticator.FormAuthenticator;
|
import org.apache.catalina.authenticator.FormAuthenticator;
|
||||||
import org.apache.catalina.connector.Request;
|
import org.apache.catalina.connector.Request;
|
||||||
import org.apache.catalina.connector.Response;
|
import org.apache.catalina.connector.Response;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
import org.keycloak.adapters.AdapterDeploymentContext;
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
import org.keycloak.adapters.AdapterTokenStore;
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
@@ -48,8 +49,6 @@ import java.io.FileInputStream;
|
|||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keycloak authentication valve
|
* Keycloak authentication valve
|
||||||
@@ -62,7 +61,7 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat
|
|||||||
|
|
||||||
public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE";
|
public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE";
|
||||||
|
|
||||||
private final static Logger log = Logger.getLogger(""+AbstractKeycloakAuthenticatorValve.class);
|
private final static Logger log = Logger.getLogger(AbstractKeycloakAuthenticatorValve.class);
|
||||||
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
|
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
|
||||||
protected AdapterDeploymentContext deploymentContext;
|
protected AdapterDeploymentContext deploymentContext;
|
||||||
protected NodesRegistrationManagement nodesRegistrationManagement;
|
protected NodesRegistrationManagement nodesRegistrationManagement;
|
||||||
@@ -118,22 +117,22 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat
|
|||||||
try {
|
try {
|
||||||
KeycloakConfigResolver configResolver = (KeycloakConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
|
KeycloakConfigResolver configResolver = (KeycloakConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
|
||||||
deploymentContext = new AdapterDeploymentContext(configResolver);
|
deploymentContext = new AdapterDeploymentContext(configResolver);
|
||||||
log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
|
log.debugv("Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()});
|
log.errorv("The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", configResolverClass, ex.getMessage());
|
||||||
deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
|
deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
InputStream configInputStream = getConfigInputStream(context);
|
InputStream configInputStream = getConfigInputStream(context);
|
||||||
KeycloakDeployment kd;
|
KeycloakDeployment kd;
|
||||||
if (configInputStream == null) {
|
if (configInputStream == null) {
|
||||||
log.warning("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
|
log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
|
||||||
kd = new KeycloakDeployment();
|
kd = new KeycloakDeployment();
|
||||||
} else {
|
} else {
|
||||||
kd = KeycloakDeploymentBuilder.build(configInputStream);
|
kd = KeycloakDeploymentBuilder.build(configInputStream);
|
||||||
}
|
}
|
||||||
deploymentContext = new AdapterDeploymentContext(kd);
|
deploymentContext = new AdapterDeploymentContext(kd);
|
||||||
log.fine("Keycloak is using a per-deployment configuration.");
|
log.debug("Keycloak is using a per-deployment configuration.");
|
||||||
}
|
}
|
||||||
|
|
||||||
context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
|
context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
|
||||||
@@ -149,8 +148,7 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat
|
|||||||
if (json == null) {
|
if (json == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
log.finest("**** using " + AdapterConstants.AUTH_DATA_PARAM_NAME);
|
log.trace("**** using " + AdapterConstants.AUTH_DATA_PARAM_NAME);
|
||||||
log.finest(json);
|
|
||||||
return new ByteArrayInputStream(json.getBytes());
|
return new ByteArrayInputStream(json.getBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,13 +157,13 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat
|
|||||||
if (is == null) {
|
if (is == null) {
|
||||||
String path = context.getServletContext().getInitParameter("keycloak.config.file");
|
String path = context.getServletContext().getInitParameter("keycloak.config.file");
|
||||||
if (path == null) {
|
if (path == null) {
|
||||||
log.finest("**** using /WEB-INF/keycloak.json");
|
log.trace("**** using /WEB-INF/keycloak.json");
|
||||||
is = context.getServletContext().getResourceAsStream("/WEB-INF/keycloak.json");
|
is = context.getServletContext().getResourceAsStream("/WEB-INF/keycloak.json");
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
is = new FileInputStream(path);
|
is = new FileInputStream(path);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
log.log(Level.SEVERE, "NOT FOUND {0}", path);
|
log.errorv("NOT FOUND {0}", path);
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>2.4.0.CR1-SNAPSHOT</version>
|
<version>3.2.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+9
-4
@@ -173,10 +173,15 @@ public class KeycloakServletExtension implements ServletExtension {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
log.debug("Setting jsession cookie path to: " + deploymentInfo.getContextPath());
|
ServletSessionConfig cookieConfig = deploymentInfo.getServletSessionConfig();
|
||||||
ServletSessionConfig cookieConfig = new ServletSessionConfig();
|
if (cookieConfig == null) {
|
||||||
cookieConfig.setPath(deploymentInfo.getContextPath());
|
cookieConfig = new ServletSessionConfig();
|
||||||
deploymentInfo.setServletSessionConfig(cookieConfig);
|
}
|
||||||
|
if (cookieConfig.getPath() == null) {
|
||||||
|
log.debug("Setting jsession cookie path to: " + deploymentInfo.getContextPath());
|
||||||
|
cookieConfig.setPath(deploymentInfo.getContextPath());
|
||||||
|
deploymentInfo.setServletSessionConfig(cookieConfig);
|
||||||
|
}
|
||||||
ChangeSessionId.turnOffChangeSessionIdOnLogin(deploymentInfo);
|
ChangeSessionId.turnOffChangeSessionIdOnLogin(deploymentInfo);
|
||||||
deploymentInfo.addListener(new ListenerInfo(UndertowNodesRegistrationManagementWrapper.class, new InstanceFactory<UndertowNodesRegistrationManagementWrapper>() {
|
deploymentInfo.addListener(new ListenerInfo(UndertowNodesRegistrationManagementWrapper.class, new InstanceFactory<UndertowNodesRegistrationManagementWrapper>() {
|
||||||
|
|
||||||
|
|||||||
Executable
+99
@@ -0,0 +1,99 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ JBoss, Home of Professional Open Source.
|
||||||
|
~ Copyright 2016 Red Hat, Inc., and individual contributors
|
||||||
|
~ as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-parent</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>3.2.0.Final</version>
|
||||||
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-wildfly-elytron-oidc-adapter</artifactId>
|
||||||
|
<name>Keycloak Wildfly Elytron OIDC Adapter</name>
|
||||||
|
<description/>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.target>1.8</maven.compiler.target>
|
||||||
|
<maven.compiler.source>1.8</maven.compiler.source>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.wildfly.common</groupId>
|
||||||
|
<artifactId>wildfly-common</artifactId>
|
||||||
|
<version>1.2.0.Beta1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.wildfly.security</groupId>
|
||||||
|
<artifactId>wildfly-elytron</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.wildfly.security.elytron-web</groupId>
|
||||||
|
<artifactId>undertow-server</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.logging</groupId>
|
||||||
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
<version>${jboss.logging.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-adapter-spi</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-adapter-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
|
<artifactId>httpclient</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcprov-jdk15on</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-annotations</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.spec.javax.servlet</groupId>
|
||||||
|
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* JBoss, Home of Professional Open Source.
|
||||||
|
* Copyright 2016 Red Hat, Inc., and individual contributors
|
||||||
|
* as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.elytron;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.OidcKeycloakAccount;
|
||||||
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
import org.wildfly.security.auth.server.SecurityIdentity;
|
||||||
|
|
||||||
|
import javax.security.auth.callback.CallbackHandler;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class ElytronAccount implements OidcKeycloakAccount {
|
||||||
|
|
||||||
|
protected static Logger log = Logger.getLogger(ElytronAccount.class);
|
||||||
|
|
||||||
|
private final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal;
|
||||||
|
|
||||||
|
public ElytronAccount(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
|
||||||
|
this.principal = principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RefreshableKeycloakSecurityContext getKeycloakSecurityContext() {
|
||||||
|
return principal.getKeycloakSecurityContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Principal getPrincipal() {
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getRoles() {
|
||||||
|
Set<String> roles = new HashSet<>();
|
||||||
|
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCurrentRequestInfo(KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
|
||||||
|
principal.getKeycloakSecurityContext().setCurrentRequestInfo(deployment, tokenStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean checkActive() {
|
||||||
|
RefreshableKeycloakSecurityContext session = getKeycloakSecurityContext();
|
||||||
|
|
||||||
|
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) {
|
||||||
|
log.debug("session is active");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("session not active");
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean tryRefresh(CallbackHandler callbackHandler) {
|
||||||
|
log.debug("Trying to refresh");
|
||||||
|
|
||||||
|
RefreshableKeycloakSecurityContext securityContext = getKeycloakSecurityContext();
|
||||||
|
|
||||||
|
if (securityContext == null) {
|
||||||
|
log.debug("No security context. Aborting refresh.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (securityContext.refreshExpiredToken(false)) {
|
||||||
|
SecurityIdentity securityIdentity = SecurityIdentityUtil.authorize(callbackHandler, principal);
|
||||||
|
|
||||||
|
if (securityIdentity != null) {
|
||||||
|
log.debug("refresh succeeded");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkActive();
|
||||||
|
}
|
||||||
|
}
|
||||||
+164
@@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
* JBoss, Home of Professional Open Source.
|
||||||
|
* Copyright 2016 Red Hat, Inc., and individual contributors
|
||||||
|
* as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.elytron;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.CookieTokenStore;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.OidcKeycloakAccount;
|
||||||
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
|
import org.wildfly.security.http.HttpScope;
|
||||||
|
import org.wildfly.security.http.Scope;
|
||||||
|
|
||||||
|
import javax.security.auth.callback.CallbackHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class ElytronCookieTokenStore implements ElytronTokeStore {
|
||||||
|
|
||||||
|
protected static Logger log = Logger.getLogger(ElytronCookieTokenStore.class);
|
||||||
|
|
||||||
|
private final ElytronHttpFacade httpFacade;
|
||||||
|
private final CallbackHandler callbackHandler;
|
||||||
|
|
||||||
|
public ElytronCookieTokenStore(ElytronHttpFacade httpFacade, CallbackHandler callbackHandler) {
|
||||||
|
this.httpFacade = httpFacade;
|
||||||
|
this.callbackHandler = callbackHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkCurrentToken() {
|
||||||
|
KeycloakDeployment deployment = httpFacade.getDeployment();
|
||||||
|
KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = CookieTokenStore.getPrincipalFromCookie(deployment, httpFacade, this);
|
||||||
|
|
||||||
|
if (principal == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext();
|
||||||
|
|
||||||
|
if (securityContext.isActive() && !securityContext.getDeployment().isAlwaysRefreshToken()) return;
|
||||||
|
|
||||||
|
// FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will
|
||||||
|
// not be updated
|
||||||
|
boolean success = securityContext.refreshExpiredToken(false);
|
||||||
|
if (success && securityContext.isActive()) return;
|
||||||
|
|
||||||
|
saveAccountInfo(new ElytronAccount(principal));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCached(RequestAuthenticator authenticator) {
|
||||||
|
KeycloakDeployment deployment = httpFacade.getDeployment();
|
||||||
|
KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = CookieTokenStore.getPrincipalFromCookie(deployment, httpFacade, this);
|
||||||
|
if (principal == null) {
|
||||||
|
log.debug("Account was not in cookie or was invalid, returning null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ElytronAccount account = new ElytronAccount(principal);
|
||||||
|
|
||||||
|
if (!deployment.getRealm().equals(account.getKeycloakSecurityContext().getRealm())) {
|
||||||
|
log.debug("Account in session belongs to a different realm than for this request.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean active = account.checkActive();
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
active = account.tryRefresh(this.callbackHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
log.debug("Cached account found");
|
||||||
|
restoreRequest();
|
||||||
|
httpFacade.authenticationComplete(account, true);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.debug("Account was not active, removing cookie and returning false");
|
||||||
|
CookieTokenStore.removeCookie(httpFacade);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveAccountInfo(OidcKeycloakAccount account) {
|
||||||
|
RefreshableKeycloakSecurityContext secContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext();
|
||||||
|
CookieTokenStore.setTokenCookie(this.httpFacade.getDeployment(), this.httpFacade, secContext);
|
||||||
|
HttpScope exchange = this.httpFacade.getScope(Scope.EXCHANGE);
|
||||||
|
|
||||||
|
exchange.registerForNotification(httpServerScopes -> logout());
|
||||||
|
|
||||||
|
exchange.setAttachment(ElytronAccount.class.getName(), account);
|
||||||
|
exchange.setAttachment(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
|
||||||
|
|
||||||
|
restoreRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout() {
|
||||||
|
logout(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) {
|
||||||
|
CookieTokenStore.setTokenCookie(this.httpFacade.getDeployment(), httpFacade, securityContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveRequest() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean restoreRequest() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout(boolean glo) {
|
||||||
|
KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = CookieTokenStore.getPrincipalFromCookie(this.httpFacade.getDeployment(), this.httpFacade, this);
|
||||||
|
|
||||||
|
if (principal == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CookieTokenStore.removeCookie(this.httpFacade);
|
||||||
|
|
||||||
|
if (glo) {
|
||||||
|
KeycloakSecurityContext ksc = (KeycloakSecurityContext) principal.getKeycloakSecurityContext();
|
||||||
|
|
||||||
|
if (ksc == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeycloakDeployment deployment = httpFacade.getDeployment();
|
||||||
|
|
||||||
|
if (!deployment.isBearerOnly() && ksc != null && ksc instanceof RefreshableKeycloakSecurityContext) {
|
||||||
|
((RefreshableKeycloakSecurityContext) ksc).logout(deployment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+394
@@ -0,0 +1,394 @@
|
|||||||
|
/*
|
||||||
|
* JBoss, Home of Professional Open Source.
|
||||||
|
* Copyright 2016 Red Hat, Inc., and individual contributors
|
||||||
|
* as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.elytron;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.cmp.Challenge;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.OIDCHttpFacade;
|
||||||
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.spi.AuthChallenge;
|
||||||
|
import org.keycloak.adapters.spi.AuthenticationError;
|
||||||
|
import org.keycloak.adapters.spi.LogoutError;
|
||||||
|
import org.keycloak.enums.TokenStore;
|
||||||
|
import org.wildfly.security.auth.server.SecurityIdentity;
|
||||||
|
import org.wildfly.security.http.HttpAuthenticationException;
|
||||||
|
import org.wildfly.security.http.HttpScope;
|
||||||
|
import org.wildfly.security.http.HttpServerCookie;
|
||||||
|
import org.wildfly.security.http.HttpServerMechanismsResponder;
|
||||||
|
import org.wildfly.security.http.HttpServerRequest;
|
||||||
|
import org.wildfly.security.http.HttpServerResponse;
|
||||||
|
import org.wildfly.security.http.Scope;
|
||||||
|
|
||||||
|
import javax.security.auth.callback.CallbackHandler;
|
||||||
|
import javax.security.cert.X509Certificate;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
class ElytronHttpFacade implements OIDCHttpFacade {
|
||||||
|
|
||||||
|
private final HttpServerRequest request;
|
||||||
|
private final CallbackHandler callbackHandler;
|
||||||
|
private final AdapterTokenStore tokenStore;
|
||||||
|
private final AdapterDeploymentContext deploymentContext;
|
||||||
|
private Consumer<HttpServerResponse> responseConsumer;
|
||||||
|
private ElytronAccount account;
|
||||||
|
private SecurityIdentity securityIdentity;
|
||||||
|
private boolean restored;
|
||||||
|
|
||||||
|
public ElytronHttpFacade(HttpServerRequest request, AdapterDeploymentContext deploymentContext, CallbackHandler handler) {
|
||||||
|
this.request = request;
|
||||||
|
this.deploymentContext = deploymentContext;
|
||||||
|
this.callbackHandler = handler;
|
||||||
|
this.tokenStore = createTokenStore();
|
||||||
|
this.responseConsumer = response -> {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void authenticationComplete(ElytronAccount account, boolean storeToken) {
|
||||||
|
this.securityIdentity = SecurityIdentityUtil.authorize(this.callbackHandler, account.getPrincipal());
|
||||||
|
|
||||||
|
if (securityIdentity != null) {
|
||||||
|
this.account = account;
|
||||||
|
RefreshableKeycloakSecurityContext keycloakSecurityContext = account.getKeycloakSecurityContext();
|
||||||
|
account.setCurrentRequestInfo(keycloakSecurityContext.getDeployment(), this.tokenStore);
|
||||||
|
if (storeToken) {
|
||||||
|
this.tokenStore.saveAccountInfo(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void authenticationComplete() {
|
||||||
|
if (securityIdentity != null) {
|
||||||
|
this.request.authenticationComplete(response -> {
|
||||||
|
if (!restored) {
|
||||||
|
responseConsumer.accept(response);
|
||||||
|
}
|
||||||
|
}, () -> ((ElytronTokeStore) tokenStore).logout(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void authenticationFailed() {
|
||||||
|
this.request.authenticationFailed("Authentication Failed", response -> responseConsumer.accept(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
void noAuthenticationInProgress() {
|
||||||
|
this.request.noAuthenticationInProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
void noAuthenticationInProgress(AuthChallenge challenge) {
|
||||||
|
if (challenge != null) {
|
||||||
|
challenge.challenge(this);
|
||||||
|
}
|
||||||
|
this.request.noAuthenticationInProgress(response -> responseConsumer.accept(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
void authenticationInProgress() {
|
||||||
|
this.request.authenticationInProgress(response -> responseConsumer.accept(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpScope getScope(Scope scope) {
|
||||||
|
return request.getScope(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpScope getScope(Scope scope, String id) {
|
||||||
|
return request.getScope(scope, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection<String> getScopeIds(Scope scope) {
|
||||||
|
return request.getScopeIds(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
AdapterTokenStore getTokenStore() {
|
||||||
|
return this.tokenStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeycloakDeployment getDeployment() {
|
||||||
|
return deploymentContext.resolveDeployment(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdapterTokenStore createTokenStore() {
|
||||||
|
KeycloakDeployment deployment = getDeployment();
|
||||||
|
|
||||||
|
if (TokenStore.SESSION.equals(deployment.getTokenStore())) {
|
||||||
|
return new ElytronSessionTokenStore(this, this.callbackHandler);
|
||||||
|
} else {
|
||||||
|
return new ElytronCookieTokenStore(this, this.callbackHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Request getRequest() {
|
||||||
|
return new Request() {
|
||||||
|
@Override
|
||||||
|
public String getMethod() {
|
||||||
|
return request.getRequestMethod();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getURI() {
|
||||||
|
try {
|
||||||
|
return URLDecoder.decode(request.getRequestURI().toString(), "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("Failed to decode request URI", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRelativePath() {
|
||||||
|
return request.getRequestPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSecure() {
|
||||||
|
return request.getRequestURI().getScheme().equals("https");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFirstParam(String param) {
|
||||||
|
throw new RuntimeException("Not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getQueryParamValue(String param) {
|
||||||
|
URI requestURI = request.getRequestURI();
|
||||||
|
String query = requestURI.getQuery();
|
||||||
|
if (query != null) {
|
||||||
|
String[] parameters = query.split("&");
|
||||||
|
for (String parameter : parameters) {
|
||||||
|
String[] keyValue = parameter.split("=");
|
||||||
|
if (keyValue[0].equals(param)) {
|
||||||
|
return keyValue[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cookie getCookie(final String cookieName) {
|
||||||
|
List<HttpServerCookie> cookies = request.getCookies();
|
||||||
|
|
||||||
|
if (cookies != null) {
|
||||||
|
for (HttpServerCookie cookie : cookies) {
|
||||||
|
if (cookie.getName().equals(cookieName)) {
|
||||||
|
return new Cookie(cookie.getName(), cookie.getValue(), cookie.getVersion(), cookie.getDomain(), cookie.getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHeader(String name) {
|
||||||
|
return request.getFirstRequestHeaderValue(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getHeaders(String name) {
|
||||||
|
return request.getRequestHeaderValues(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return request.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRemoteAddr() {
|
||||||
|
InetSocketAddress sourceAddress = request.getSourceAddress();
|
||||||
|
if (sourceAddress == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
InetAddress address = sourceAddress.getAddress();
|
||||||
|
if (address == null) {
|
||||||
|
// this is unresolved, so we just return the host name not exactly spec, but if the name should be
|
||||||
|
// resolved then a PeerNameResolvingHandler should be used and this is probably better than just
|
||||||
|
// returning null
|
||||||
|
return sourceAddress.getHostString();
|
||||||
|
}
|
||||||
|
return address.getHostAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setError(AuthenticationError error) {
|
||||||
|
request.getScope(Scope.EXCHANGE).setAttachment(AuthenticationError.class.getName(), error);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setError(LogoutError error) {
|
||||||
|
request.getScope(Scope.EXCHANGE).setAttachment(LogoutError.class.getName(), error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response getResponse() {
|
||||||
|
return new Response() {
|
||||||
|
@Override
|
||||||
|
public void setStatus(final int status) {
|
||||||
|
responseConsumer = responseConsumer.andThen(response -> response.setStatusCode(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addHeader(final String name, final String value) {
|
||||||
|
responseConsumer = responseConsumer.andThen(response -> response.addResponseHeader(name, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHeader(String name, String value) {
|
||||||
|
addHeader(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resetCookie(final String name, final String path) {
|
||||||
|
responseConsumer = responseConsumer.andThen(response -> setCookie(name, "", path, null, 0, false, false, response));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCookie(final String name, final String value, final String path, final String domain, final int maxAge, final boolean secure, final boolean httpOnly) {
|
||||||
|
responseConsumer = responseConsumer.andThen(response -> setCookie(name, value, path, domain, maxAge, secure, httpOnly, response));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCookie(final String name, final String value, final String path, final String domain, final int maxAge, final boolean secure, final boolean httpOnly, HttpServerResponse response) {
|
||||||
|
response.setResponseCookie(new HttpServerCookie() {
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDomain() {
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMaxAge() {
|
||||||
|
return maxAge;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPath() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSecure() {
|
||||||
|
return secure;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVersion() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isHttpOnly() {
|
||||||
|
return httpOnly;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream() {
|
||||||
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||||
|
responseConsumer = responseConsumer.andThen(new Consumer<HttpServerResponse>() {
|
||||||
|
@Override
|
||||||
|
public void accept(HttpServerResponse httpServerResponse) {
|
||||||
|
try {
|
||||||
|
httpServerResponse.getOutputStream().write(stream.toByteArray());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to write to response output stream", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendError(int code) {
|
||||||
|
setStatus(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendError(final int code, final String message) {
|
||||||
|
responseConsumer = responseConsumer.andThen(response -> {
|
||||||
|
response.setStatusCode(code);
|
||||||
|
response.addResponseHeader("Content-Type", "text/html");
|
||||||
|
try {
|
||||||
|
response.getOutputStream().write(message.getBytes());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void end() {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X509Certificate[] getCertificateChain() {
|
||||||
|
return new X509Certificate[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeycloakSecurityContext getSecurityContext() {
|
||||||
|
if (account == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.account.getKeycloakSecurityContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean restoreRequest() {
|
||||||
|
restored = this.request.resumeRequest();
|
||||||
|
return restored;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void suspendRequest() {
|
||||||
|
responseConsumer = responseConsumer.andThen(httpServerResponse -> request.suspendRequest());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAuthorized() {
|
||||||
|
return this.securityIdentity != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* JBoss, Home of Professional Open Source.
|
||||||
|
* Copyright 2016 Red Hat, Inc., and individual contributors
|
||||||
|
* as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.elytron;
|
||||||
|
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.adapters.BearerTokenRequestAuthenticator;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.OAuthRequestAuthenticator;
|
||||||
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
|
import org.keycloak.adapters.spi.AuthOutcome;
|
||||||
|
import org.wildfly.security.http.HttpScope;
|
||||||
|
import org.wildfly.security.http.Scope;
|
||||||
|
|
||||||
|
import javax.security.auth.callback.CallbackHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class ElytronRequestAuthenticator extends RequestAuthenticator {
|
||||||
|
|
||||||
|
public ElytronRequestAuthenticator(CallbackHandler callbackHandler, ElytronHttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort) {
|
||||||
|
super(facade, deployment, facade.getTokenStore(), sslRedirectPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthOutcome authenticate() {
|
||||||
|
AuthOutcome authenticate = super.authenticate();
|
||||||
|
|
||||||
|
if (AuthOutcome.AUTHENTICATED.equals(authenticate)) {
|
||||||
|
if (!getElytronHttpFacade().isAuthorized()) {
|
||||||
|
return AuthOutcome.FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return authenticate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected OAuthRequestAuthenticator createOAuthAuthenticator() {
|
||||||
|
return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort, tokenStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void completeOAuthAuthentication(final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
|
||||||
|
getElytronHttpFacade().authenticationComplete(new ElytronAccount(principal), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal, String method) {
|
||||||
|
getElytronHttpFacade().authenticationComplete(new ElytronAccount(principal), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String changeHttpSessionId(boolean create) {
|
||||||
|
HttpScope session = getElytronHttpFacade().getScope(Scope.SESSION);
|
||||||
|
|
||||||
|
if (create) {
|
||||||
|
if (!session.exists()) {
|
||||||
|
session.create();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return session != null ? session.getID() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ElytronHttpFacade getElytronHttpFacade() {
|
||||||
|
return (ElytronHttpFacade) facade;
|
||||||
|
}
|
||||||
|
}
|
||||||
+202
@@ -0,0 +1,202 @@
|
|||||||
|
/*
|
||||||
|
* JBoss, Home of Professional Open Source.
|
||||||
|
* Copyright 2016 Red Hat, Inc., and individual contributors
|
||||||
|
* as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.elytron;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import javax.security.auth.callback.CallbackHandler;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.AdapterUtils;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.OidcKeycloakAccount;
|
||||||
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
|
import org.wildfly.security.http.HttpScope;
|
||||||
|
import org.wildfly.security.http.HttpScopeNotification;
|
||||||
|
import org.wildfly.security.http.Scope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class ElytronSessionTokenStore implements ElytronTokeStore {
|
||||||
|
|
||||||
|
private static Logger log = Logger.getLogger(ElytronSessionTokenStore.class);
|
||||||
|
|
||||||
|
private final ElytronHttpFacade httpFacade;
|
||||||
|
private final CallbackHandler callbackHandler;
|
||||||
|
|
||||||
|
public ElytronSessionTokenStore(ElytronHttpFacade httpFacade, CallbackHandler callbackHandler) {
|
||||||
|
this.httpFacade = httpFacade;
|
||||||
|
this.callbackHandler = callbackHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkCurrentToken() {
|
||||||
|
HttpScope session = httpFacade.getScope(Scope.SESSION);
|
||||||
|
if (!session.exists()) return;
|
||||||
|
RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) session.getAttachment(KeycloakSecurityContext.class.getName());
|
||||||
|
if (securityContext == null) return;
|
||||||
|
|
||||||
|
// just in case session got serialized
|
||||||
|
if (securityContext.getDeployment() == null) securityContext.setCurrentRequestInfo(httpFacade.getDeployment(), this);
|
||||||
|
|
||||||
|
if (securityContext.isActive() && !securityContext.getDeployment().isAlwaysRefreshToken()) return;
|
||||||
|
|
||||||
|
// FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will
|
||||||
|
// not be updated
|
||||||
|
boolean success = securityContext.refreshExpiredToken(false);
|
||||||
|
if (success && securityContext.isActive()) return;
|
||||||
|
|
||||||
|
// Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session
|
||||||
|
session.setAttachment(KeycloakSecurityContext.class.getName(), null);
|
||||||
|
session.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCached(RequestAuthenticator authenticator) {
|
||||||
|
HttpScope session = this.httpFacade.getScope(Scope.SESSION);
|
||||||
|
|
||||||
|
if (session == null || !session.supportsAttachments()) {
|
||||||
|
log.debug("session was null, returning null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ElytronAccount account;
|
||||||
|
|
||||||
|
try {
|
||||||
|
account = (ElytronAccount) session.getAttachment(ElytronAccount.class.getName());
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
log.debug("session was invalidated. Return false.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (account == null) {
|
||||||
|
log.debug("Account was not in session, returning null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeycloakDeployment deployment = httpFacade.getDeployment();
|
||||||
|
|
||||||
|
if (!deployment.getRealm().equals(account.getKeycloakSecurityContext().getRealm())) {
|
||||||
|
log.debug("Account in session belongs to a different realm than for this request.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean active = account.checkActive();
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
active = account.tryRefresh(this.callbackHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
log.debug("Cached account found");
|
||||||
|
restoreRequest();
|
||||||
|
httpFacade.authenticationComplete(account, true);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.debug("Refresh failed. Account was not active. Returning null and invalidating Http session");
|
||||||
|
try {
|
||||||
|
session.setAttachment(KeycloakSecurityContext.class.getName(), null);
|
||||||
|
session.setAttachment(ElytronAccount.class.getName(), null);
|
||||||
|
session.invalidate();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Failed to invalidate session, might already be invalidated");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveAccountInfo(OidcKeycloakAccount account) {
|
||||||
|
HttpScope session = this.httpFacade.getScope(Scope.SESSION);
|
||||||
|
|
||||||
|
if (!session.exists()) {
|
||||||
|
session.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
session.setAttachment(ElytronAccount.class.getName(), account);
|
||||||
|
session.setAttachment(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
|
||||||
|
|
||||||
|
session.registerForNotification(httpScopeNotification -> {
|
||||||
|
if (!httpScopeNotification.isOfType(HttpScopeNotification.SessionNotificationType.UNDEPLOY)) {
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
HttpScope scope = this.httpFacade.getScope(Scope.EXCHANGE);
|
||||||
|
|
||||||
|
scope.setAttachment(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout() {
|
||||||
|
logout(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) {
|
||||||
|
KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = new KeycloakPrincipal<RefreshableKeycloakSecurityContext>(AdapterUtils.getPrincipalName(this.httpFacade.getDeployment(), securityContext.getToken()), securityContext);
|
||||||
|
saveAccountInfo(new ElytronAccount(principal));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveRequest() {
|
||||||
|
this.httpFacade.suspendRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean restoreRequest() {
|
||||||
|
return this.httpFacade.restoreRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout(boolean glo) {
|
||||||
|
HttpScope session = this.httpFacade.getScope(Scope.SESSION);
|
||||||
|
|
||||||
|
if (!session.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (glo) {
|
||||||
|
KeycloakSecurityContext ksc = (KeycloakSecurityContext) session.getAttachment(KeycloakSecurityContext.class.getName());
|
||||||
|
|
||||||
|
if (ksc == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeycloakDeployment deployment = httpFacade.getDeployment();
|
||||||
|
|
||||||
|
if (!deployment.isBearerOnly() && ksc != null && ksc instanceof RefreshableKeycloakSecurityContext) {
|
||||||
|
((RefreshableKeycloakSecurityContext) ksc).logout(deployment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.setAttachment(KeycloakSecurityContext.class.getName(), null);
|
||||||
|
session.setAttachment(ElytronAccount.class.getName(), null);
|
||||||
|
session.invalidate();
|
||||||
|
} catch (IllegalStateException ise) {
|
||||||
|
// Session may be already logged-out in case that app has adminUrl
|
||||||
|
log.debugf("Session %s logged-out already", session.getID());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.elytron;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public interface ElytronTokeStore extends AdapterTokenStore {
|
||||||
|
void logout(boolean glo);
|
||||||
|
}
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* JBoss, Home of Professional Open Source.
|
||||||
|
* Copyright 2016 Red Hat, Inc., and individual contributors
|
||||||
|
* as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.elytron;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.KeycloakConfigResolver;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||||
|
import org.keycloak.constants.AdapterConstants;
|
||||||
|
|
||||||
|
import javax.servlet.ServletContext;
|
||||||
|
import javax.servlet.ServletContextEvent;
|
||||||
|
import javax.servlet.ServletContextListener;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>A {@link ServletContextListener} that parses the keycloak adapter configuration and set the same configuration
|
||||||
|
* as a {@link ServletContext} attribute in order to provide to {@link KeycloakHttpServerAuthenticationMechanism} a way
|
||||||
|
* to obtain the configuration when processing requests.
|
||||||
|
*
|
||||||
|
* <p>This listener should be automatically registered to a deployment using the subsystem.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class KeycloakConfigurationServletListener implements ServletContextListener {
|
||||||
|
@Override
|
||||||
|
public void contextInitialized(ServletContextEvent sce) {
|
||||||
|
ServletContext servletContext = sce.getServletContext();
|
||||||
|
String configResolverClass = servletContext.getInitParameter("keycloak.config.resolver");
|
||||||
|
KeycloakConfigResolver configResolver;
|
||||||
|
AdapterDeploymentContext deploymentContext;
|
||||||
|
|
||||||
|
if (configResolverClass != null) {
|
||||||
|
try {
|
||||||
|
configResolver = (KeycloakConfigResolver) servletContext.getClassLoader().loadClass(configResolverClass).newInstance();
|
||||||
|
deploymentContext = new AdapterDeploymentContext(configResolver);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
InputStream is = getConfigInputStream(servletContext);
|
||||||
|
|
||||||
|
KeycloakDeployment deployment;
|
||||||
|
|
||||||
|
if (is == null) {
|
||||||
|
deployment = new KeycloakDeployment();
|
||||||
|
} else {
|
||||||
|
deployment = KeycloakDeploymentBuilder.build(is);
|
||||||
|
}
|
||||||
|
|
||||||
|
deploymentContext = new AdapterDeploymentContext(deployment);
|
||||||
|
}
|
||||||
|
|
||||||
|
servletContext.setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void contextDestroyed(ServletContextEvent sce) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputStream getConfigInputStream(ServletContext servletContext) {
|
||||||
|
InputStream is = getJSONFromServletContext(servletContext);
|
||||||
|
|
||||||
|
if (is == null) {
|
||||||
|
String path = servletContext.getInitParameter("keycloak.config.file");
|
||||||
|
|
||||||
|
if (path == null) {
|
||||||
|
is = servletContext.getResourceAsStream("/WEB-INF/keycloak.json");
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
is = new FileInputStream(path);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return is;
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputStream getJSONFromServletContext(ServletContext servletContext) {
|
||||||
|
String json = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME);
|
||||||
|
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ByteArrayInputStream(json.getBytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 3661 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user