Compare commits
630
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d3287fe47 | ||
|
|
01ddd50b96 | ||
|
|
3335e8630b | ||
|
|
ebd3d522e2 | ||
|
|
04179c5681 | ||
|
|
6c8ebdb456 | ||
|
|
85446c3b46 | ||
|
|
df7a68b709 | ||
|
|
1c65fd86ef | ||
|
|
7f9e6bbf60 | ||
|
|
2ba21485f9 | ||
|
|
c998198aac | ||
|
|
3e8d4e4eb6 | ||
|
|
b15918b745 | ||
|
|
d2b7f18480 | ||
|
|
663b99d884 | ||
|
|
e3e18913f1 | ||
|
|
f98c454775 | ||
|
|
9c2944c090 | ||
|
|
66eb9095c1 | ||
|
|
e7c73518b3 | ||
|
|
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 | ||
|
|
93cec9b3ee | ||
|
|
20c5c6b60c | ||
|
|
f29bb7d501 | ||
|
|
c3d9859c6e | ||
|
|
3b3b219a86 | ||
|
|
3f2e9c3592 | ||
|
|
787a3f8fcc | ||
|
|
a4cbf130b4 | ||
|
|
1c0e23db66 | ||
|
|
f33a1e2143 | ||
|
|
63ca9b2fe8 | ||
|
|
5453bec1bf | ||
|
|
e65dd4fbc3 | ||
|
|
7b115b4b44 | ||
|
|
4d64f0e160 | ||
|
|
7453e96f5c | ||
|
|
9be9d3f580 | ||
|
|
7d51df4eed | ||
|
|
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 | ||
|
|
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 | ||
|
|
3e71aeddf3 | ||
|
|
a2e00e4c3b | ||
|
|
2c6e9be8e5 | ||
|
|
fce41c0ab8 | ||
|
|
ca5c806f58 | ||
|
|
1e8f5f1a51 | ||
|
|
b434c2b9cf | ||
|
|
60afd280c3 | ||
|
|
9b2ef96b22 | ||
|
|
44ee53b0d8 | ||
|
|
cc9131441e | ||
|
|
5e496fb9bb | ||
|
|
5c089e45d4 | ||
|
|
a27be0cee7 | ||
|
|
a0ae8c126e | ||
|
|
55556fc63c | ||
|
|
8fc5b00d91 | ||
|
|
475c6403cc | ||
|
|
cf17687b8b | ||
|
|
50339f6f0e | ||
|
|
26b1541f4a | ||
|
|
1c3a475d1e | ||
|
|
65136fabdd | ||
|
|
70d1ac7162 | ||
|
|
b1f35ff974 | ||
|
|
f5d4554b39 | ||
|
|
8a0cb507c5 | ||
|
|
89a43b8771 | ||
|
|
83571dba5d | ||
|
|
a6d59faf98 | ||
|
|
9512180406 | ||
|
|
0f0f20716f | ||
|
|
b4f072ed81 | ||
|
|
394a9daa64 | ||
|
|
a3beef754c | ||
|
|
94afba91a0 | ||
|
|
8794416241 | ||
|
|
f471ad46c9 | ||
|
|
989a701e95 | ||
|
|
37c1c84e04 | ||
|
|
39ffd3d756 | ||
|
|
b920c7b2c4 | ||
|
|
a2c54b03c8 | ||
|
|
7666387277 | ||
|
|
cc0eb47814 | ||
|
|
c280634bfa | ||
|
|
fb1cd9d27d | ||
|
|
b612415a88 | ||
|
|
223041bc6b | ||
|
|
ab0514041e | ||
|
|
750e942267 | ||
|
|
17c13043d0 | ||
|
|
750075d0dd | ||
|
|
9df43e3bd2 | ||
|
|
d1eaace4cf | ||
|
|
39f40bc005 | ||
|
|
6c64494620 | ||
|
|
c9e3fe9a92 | ||
|
|
4ce055cede | ||
|
|
1e65174867 | ||
|
|
1052c71124 | ||
|
|
a86b5988b5 | ||
|
|
088f0ea630 | ||
|
|
08dbdeb57f | ||
|
|
8a5f817030 | ||
|
|
675faee593 | ||
|
|
7e33f4a7d1 | ||
|
|
71c88a8dfc | ||
|
|
52a4509ada | ||
|
|
1ee6df412e | ||
|
|
94076a3b24 | ||
|
|
43002f7a8a | ||
|
|
8816b55843 | ||
|
|
9c724b616d | ||
|
|
025cf5ebaf | ||
|
|
6f1b8e1fee | ||
|
|
80b071024f | ||
|
|
4880c0443c | ||
|
|
c05057748f | ||
|
|
de7006a048 | ||
|
|
2b6811a366 | ||
|
|
10f9800327 | ||
|
|
292777259e | ||
|
|
01c42f9359 | ||
|
|
ef48594d85 | ||
|
|
c9a58fad15 | ||
|
|
5b54375490 | ||
|
|
db4f3561a5 | ||
|
|
f138eecc27 | ||
|
|
5a86623c88 | ||
|
|
14dc0ff92f | ||
|
|
4302b440ee | ||
|
|
c75dcb90c2 | ||
|
|
3efd103712 | ||
|
|
f922164a88 | ||
|
|
36257863db | ||
|
|
14f96fdb4b | ||
|
|
570d71c07b | ||
|
|
8ae1b1740d | ||
|
|
4f9e35c0a1 | ||
|
|
67bb9aef3d | ||
|
|
1ae268ec6f | ||
|
|
10deac0b06 | ||
|
|
057cc37b60 | ||
|
|
70a8255eae | ||
|
|
d5c3bde0af | ||
|
|
5d840500af | ||
|
|
904a5c3ca5 | ||
|
|
66698f60d4 | ||
|
|
3035cbc5db | ||
|
|
6c18b300a4 | ||
|
|
6baf9b89fe | ||
|
|
fd5f7eb165 | ||
|
|
76c37de1e8 | ||
|
|
c3f3c5c6a5 | ||
|
|
f2b28d8215 | ||
|
|
672f5d879b | ||
|
|
20a6795f74 | ||
|
|
c1ef4067bc | ||
|
|
c1fb71c031 | ||
|
|
136f3bc5f9 | ||
|
|
2052a0dff1 | ||
|
|
ee8c986e8e | ||
|
|
b58bfe56d6 | ||
|
|
6c5d1b9214 | ||
|
|
97be09a3bc | ||
|
|
6605ba1362 | ||
|
|
ac18b6c71e | ||
|
|
d9069e0955 | ||
|
|
3f98f23422 | ||
|
|
706c1e2660 | ||
|
|
95d2130405 | ||
|
|
683428a2c2 | ||
|
|
a315919dfc | ||
|
|
1b89472451 | ||
|
|
ae2a1e929f | ||
|
|
ad995934b0 | ||
|
|
c5b6507830 | ||
|
|
d7c5f991c8 | ||
|
|
cef1973ee4 | ||
|
|
d98c375495 | ||
|
|
da516a78b3 | ||
|
|
a0f359c93c | ||
|
|
4bf5da8fac | ||
|
|
44977207e3 | ||
|
|
facdd586a3 | ||
|
|
893963f339 | ||
|
|
a5e5f4cf9c | ||
|
|
fdb7924d3f | ||
|
|
782620655f | ||
|
|
446b57b827 | ||
|
|
2c287af977 | ||
|
|
4bd567e610 | ||
|
|
35b0872ed9 | ||
|
|
89348af59e | ||
|
|
ccaac40863 | ||
|
|
33bd7ca45f | ||
|
|
408850e7bd | ||
|
|
f4a77c3d06 | ||
|
|
3ea555bae6 | ||
|
|
c543035f5a | ||
|
|
b6b567f948 | ||
|
|
479295cfd2 | ||
|
|
a78cfa4b2c | ||
|
|
c6caeb3bec | ||
|
|
5f58c96258 | ||
|
|
bb1d255c40 | ||
|
|
a9d47287ee | ||
|
|
3d46b4c425 | ||
|
|
db428dad1d | ||
|
|
4cc44bc174 | ||
|
|
3e5f490882 | ||
|
|
7579606ac4 | ||
|
|
ee4712b2e9 | ||
|
|
2d1a458f5d | ||
|
|
e958bd254a | ||
|
|
0c6b47b9f2 | ||
|
|
4cea1919f6 | ||
|
|
91da6a47d7 | ||
|
|
f13d759a61 | ||
|
|
21e423ad5f | ||
|
|
c6ac3266f0 | ||
|
|
621d234adc | ||
|
|
ab72b2b141 | ||
|
|
e8dd05619b | ||
|
|
25909b5e79 | ||
|
|
5925a99800 | ||
|
|
6e773c84e8 | ||
|
|
73e3f2a89b | ||
|
|
69dddfa73a | ||
|
|
3b8972e027 | ||
|
|
50b181e804 | ||
|
|
21459d0f43 | ||
|
|
fbe1fd8f98 | ||
|
|
a8c844828d | ||
|
|
c0959fc562 | ||
|
|
6a86bb8ebe | ||
|
|
76e2598c5c | ||
|
|
bf5e724f00 | ||
|
|
112538a220 | ||
|
|
23ea1f8939 | ||
|
|
93f9def3e8 | ||
|
|
8a7703b0ef | ||
|
|
2578df1a82 | ||
|
|
8dca101069 | ||
|
|
4ee759d9ec | ||
|
|
3129e392b0 | ||
|
|
0d0d614b99 | ||
|
|
3fec344fa3 | ||
|
|
f8a78d5565 | ||
|
|
68e853b4bd | ||
|
|
2cc4a920a0 | ||
|
|
b67cb0e97a | ||
|
|
1b2f6b763a | ||
|
|
1c2f49ab4e | ||
|
|
3c237760b6 | ||
|
|
859c5cbe1e | ||
|
|
28f31ce57f | ||
|
|
317749a7fd | ||
|
|
94f33473a8 | ||
|
|
04f2b9ac01 | ||
|
|
7882a6513c | ||
|
|
7e6c49fc74 | ||
|
|
765c3a7b0d | ||
|
|
f3df185bb5 | ||
|
|
8c42167302 | ||
|
|
4b27e66714 | ||
|
|
3e28ac1e46 | ||
|
|
a119a46495 | ||
|
|
3ca836ffdc | ||
|
|
bc718471da | ||
|
|
e24b27c79e | ||
|
|
dcaac76b4b | ||
|
|
d741349830 | ||
|
|
cd3f9ee27c | ||
|
|
3320d34c2b | ||
|
|
036407fd90 | ||
|
|
8bca84c0e7 | ||
|
|
f7a6d39b01 | ||
|
|
c79378ca5f | ||
|
|
4d47f758fc | ||
|
|
e390cffa63 | ||
|
|
c615674cbb | ||
|
|
1a4f9e656d | ||
|
|
e1a214351f | ||
|
|
9801f09a93 | ||
|
|
9f8fb98083 | ||
|
|
fd86f3bda8 | ||
|
|
390becb935 | ||
|
|
7d8c79b17b | ||
|
|
772a1580ff | ||
|
|
5a00aaefa8 | ||
|
|
1bf24d26a4 | ||
|
|
284f49e584 | ||
|
|
e7ab53dd5f | ||
|
|
139158e614 | ||
|
|
c4ad84945c | ||
|
|
94d4afa11c | ||
|
|
d7d91cfbc0 | ||
|
|
757d5c16df | ||
|
|
36c2422fa4 | ||
|
|
ec0f222f7e | ||
|
|
969b44768a | ||
|
|
db0024d857 | ||
|
|
d59f6e397f | ||
|
|
072ccb5c61 | ||
|
|
8ee566f5fa | ||
|
|
8e9db1be96 | ||
|
|
115f95d90e | ||
|
|
2bba6af6d9 | ||
|
|
0d3a50411e | ||
|
|
3bed84d712 | ||
|
|
0485f5c43d |
No files matched your search
+8
-1
@@ -11,6 +11,13 @@ env:
|
||||
global:
|
||||
- MAVEN_SKIP_RC=true
|
||||
- MAVEN_OPTS="-Xms512m -Xmx2048m"
|
||||
matrix:
|
||||
- TESTS=old
|
||||
- TESTS=group1
|
||||
- TESTS=group2
|
||||
- TESTS=group3
|
||||
- TESTS=group4
|
||||
- TESTS=adapter
|
||||
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
@@ -22,6 +29,6 @@ install:
|
||||
- travis_wait 60 mvn install -Pdistribution -DskipTests=true -B -V -q
|
||||
|
||||
script:
|
||||
- mvn test -B
|
||||
- ./travis-run-tests.sh $TESTS
|
||||
|
||||
sudo: false
|
||||
@@ -9,7 +9,7 @@ For more information about Keycloak visit [Keycloak homepage](http://keycloak.or
|
||||
Building
|
||||
--------
|
||||
|
||||
Ensure you have JDK 8 (or newer), Maven 3.2.1 (or newer) and Git installed
|
||||
Ensure you have JDK 8 (or newer), Maven 3.1.1 (or newer) and Git installed
|
||||
|
||||
java -version
|
||||
mvn -version
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
+16
@@ -342,6 +342,12 @@ public class AdapterDeploymentContext {
|
||||
return delegate.getNotBefore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateNotBefore(int notBefore) {
|
||||
delegate.setNotBefore(notBefore);
|
||||
getPublicKeyLocator().reset(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExposeToken(boolean exposeToken) {
|
||||
delegate.setExposeToken(exposeToken);
|
||||
@@ -446,6 +452,16 @@ public class AdapterDeploymentContext {
|
||||
public int 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) {
|
||||
|
||||
+6
-2
@@ -142,9 +142,13 @@ public class AuthenticatedActionsHandler {
|
||||
AuthorizationContext authorizationContext = policyEnforcer.enforce(facade);
|
||||
RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) facade.getSecurityContext();
|
||||
|
||||
session.setAuthorizationContext(authorizationContext);
|
||||
if (session != null) {
|
||||
session.setAuthorizationContext(authorizationContext);
|
||||
|
||||
return authorizationContext.isGranted();
|
||||
return authorizationContext.isGranted();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to enforce policy decisions.", e);
|
||||
}
|
||||
|
||||
+1
-8
@@ -46,14 +46,7 @@ public class HttpAdapterUtils {
|
||||
}
|
||||
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);
|
||||
return JsonSerialization.readValue(json, clazz);
|
||||
return JsonSerialization.readValue(is, clazz);
|
||||
} finally {
|
||||
try {
|
||||
is.close();
|
||||
|
||||
+4
-4
@@ -38,7 +38,7 @@ import org.apache.http.params.BasicHttpParams;
|
||||
import org.apache.http.params.HttpConnectionParams;
|
||||
import org.keycloak.common.util.EnvUtil;
|
||||
import org.keycloak.common.util.KeystoreUtil;
|
||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||
import org.keycloak.representations.adapters.config.AdapterHttpClientConfig;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
@@ -333,7 +333,7 @@ public class HttpClientBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
public HttpClient build(AdapterConfig adapterConfig) {
|
||||
public HttpClient build(AdapterHttpClientConfig adapterConfig) {
|
||||
disableCookieCache(); // disable cookie cache as we don't want sticky sessions for load balancing
|
||||
|
||||
String truststorePath = adapterConfig.getTruststore();
|
||||
@@ -379,13 +379,13 @@ public class HttpClientBuilder {
|
||||
/**
|
||||
* Configures a the proxy to use for auth-server requests if provided.
|
||||
* <p>
|
||||
* If the given {@link AdapterConfig} contains the attribute {@code proxy-url} we use the
|
||||
* If the given {@link AdapterHttpClientConfig} contains the attribute {@code proxy-url} we use the
|
||||
* given URL as a proxy server, otherwise the proxy configuration is ignored.
|
||||
* </p>
|
||||
*
|
||||
* @param adapterConfig
|
||||
*/
|
||||
private void configureProxyForAuthServerIfProvided(AdapterConfig adapterConfig) {
|
||||
private void configureProxyForAuthServerIfProvided(AdapterHttpClientConfig adapterConfig) {
|
||||
|
||||
if (adapterConfig == null || adapterConfig.getProxyUrl() == null || adapterConfig.getProxyUrl().trim().isEmpty()) {
|
||||
return;
|
||||
|
||||
+23
@@ -57,6 +57,7 @@ public class KeycloakDeployment {
|
||||
|
||||
protected String resourceName;
|
||||
protected boolean bearerOnly;
|
||||
protected boolean autodetectBearerOnly;
|
||||
protected boolean enableBasicAuth;
|
||||
protected boolean publicClient;
|
||||
protected Map<String, Object> resourceCredentials = new HashMap<>();
|
||||
@@ -81,6 +82,7 @@ public class KeycloakDeployment {
|
||||
protected volatile int notBefore;
|
||||
protected int tokenMinimumTimeToLive;
|
||||
protected int minTimeBetweenJwksRequests;
|
||||
protected int publicKeyCacheTtl;
|
||||
private PolicyEnforcer policyEnforcer;
|
||||
|
||||
public KeycloakDeployment() {
|
||||
@@ -200,6 +202,14 @@ public class KeycloakDeployment {
|
||||
this.bearerOnly = bearerOnly;
|
||||
}
|
||||
|
||||
public boolean isAutodetectBearerOnly() {
|
||||
return autodetectBearerOnly;
|
||||
}
|
||||
|
||||
public void setAutodetectBearerOnly(boolean autodetectBearerOnly) {
|
||||
this.autodetectBearerOnly = autodetectBearerOnly;
|
||||
}
|
||||
|
||||
public boolean isEnableBasicAuth() {
|
||||
return enableBasicAuth;
|
||||
}
|
||||
@@ -328,6 +338,11 @@ public class KeycloakDeployment {
|
||||
this.notBefore = notBefore;
|
||||
}
|
||||
|
||||
public void updateNotBefore(int notBefore) {
|
||||
this.notBefore = notBefore;
|
||||
getPublicKeyLocator().reset(this);
|
||||
}
|
||||
|
||||
public boolean isAlwaysRefreshToken() {
|
||||
return alwaysRefreshToken;
|
||||
}
|
||||
@@ -384,6 +399,14 @@ public class KeycloakDeployment {
|
||||
this.minTimeBetweenJwksRequests = minTimeBetweenJwksRequests;
|
||||
}
|
||||
|
||||
public int getPublicKeyCacheTtl() {
|
||||
return publicKeyCacheTtl;
|
||||
}
|
||||
|
||||
public void setPublicKeyCacheTtl(int publicKeyCacheTtl) {
|
||||
this.publicKeyCacheTtl = publicKeyCacheTtl;
|
||||
}
|
||||
|
||||
public void setPolicyEnforcer(PolicyEnforcer policyEnforcer) {
|
||||
this.policyEnforcer = policyEnforcer;
|
||||
}
|
||||
|
||||
+2
@@ -99,12 +99,14 @@ public class KeycloakDeploymentBuilder {
|
||||
}
|
||||
|
||||
deployment.setBearerOnly(adapterConfig.isBearerOnly());
|
||||
deployment.setAutodetectBearerOnly(adapterConfig.isAutodetectBearerOnly());
|
||||
deployment.setEnableBasicAuth(adapterConfig.isEnableBasicAuth());
|
||||
deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken());
|
||||
deployment.setRegisterNodeAtStartup(adapterConfig.isRegisterNodeAtStartup());
|
||||
deployment.setRegisterNodePeriod(adapterConfig.getRegisterNodePeriod());
|
||||
deployment.setTokenMinimumTimeToLive(adapterConfig.getTokenMinimumTimeToLive());
|
||||
deployment.setMinTimeBetweenJwksRequests(adapterConfig.getMinTimeBetweenJwksRequests());
|
||||
deployment.setPublicKeyCacheTtl(adapterConfig.getPublicKeyCacheTtl());
|
||||
|
||||
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");
|
||||
|
||||
+4
-3
@@ -25,6 +25,7 @@ import org.keycloak.adapters.spi.AuthChallenge;
|
||||
import org.keycloak.adapters.spi.AuthOutcome;
|
||||
import org.keycloak.adapters.spi.HttpFacade;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.util.Encode;
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
import org.keycloak.common.util.UriUtils;
|
||||
import org.keycloak.constants.AdapterConstants;
|
||||
@@ -169,7 +170,7 @@ public class OAuthRequestAuthenticator {
|
||||
KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone()
|
||||
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
||||
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
|
||||
.queryParam(OAuth2Constants.REDIRECT_URI, url)
|
||||
.queryParam(OAuth2Constants.REDIRECT_URI, Encode.encodeQueryParamAsIs(url)) // Need to encode uri ourselves as queryParam() will not encode % characters.
|
||||
.queryParam(OAuth2Constants.STATE, state)
|
||||
.queryParam("login", "true");
|
||||
if(loginHint != null && loginHint.length() > 0){
|
||||
@@ -203,7 +204,7 @@ public class OAuthRequestAuthenticator {
|
||||
|
||||
protected AuthChallenge loginRedirect() {
|
||||
final String state = getStateCode();
|
||||
final String redirect = getRedirectUri(state);
|
||||
final String redirect = getRedirectUri(state);
|
||||
if (redirect == null) {
|
||||
return challenge(403, OIDCAuthenticationError.Reason.NO_REDIRECT_URI, null);
|
||||
}
|
||||
@@ -357,7 +358,7 @@ public class OAuthRequestAuthenticator {
|
||||
return challenge(403, OIDCAuthenticationError.Reason.INVALID_TOKEN, null);
|
||||
}
|
||||
if (tokenResponse.getNotBeforePolicy() > deployment.getNotBefore()) {
|
||||
deployment.setNotBefore(tokenResponse.getNotBeforePolicy());
|
||||
deployment.updateNotBefore(tokenResponse.getNotBeforePolicy());
|
||||
}
|
||||
if (token.getIssuedAt() < deployment.getNotBefore()) {
|
||||
log.error("Stale token");
|
||||
|
||||
+2
-2
@@ -155,7 +155,7 @@ public class PreAuthActionsHandler {
|
||||
} else {
|
||||
log.debugf("logout of all sessions for application '%s'", action.getResource());
|
||||
if (action.getNotBefore() > deployment.getNotBefore()) {
|
||||
deployment.setNotBefore(action.getNotBefore());
|
||||
deployment.updateNotBefore(action.getNotBefore());
|
||||
}
|
||||
userSessionManagement.logoutAll();
|
||||
}
|
||||
@@ -177,7 +177,7 @@ public class PreAuthActionsHandler {
|
||||
}
|
||||
PushNotBeforeAction action = JsonSerialization.readValue(token.getContent(), PushNotBeforeAction.class);
|
||||
if (!validateAction(action)) return;
|
||||
deployment.setNotBefore(action.getNotBefore());
|
||||
deployment.updateNotBefore(action.getNotBefore());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
+1
-1
@@ -144,7 +144,7 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
|
||||
}
|
||||
|
||||
if (response.getNotBeforePolicy() > deployment.getNotBefore()) {
|
||||
deployment.setNotBefore(response.getNotBeforePolicy());
|
||||
deployment.updateNotBefore(response.getNotBeforePolicy());
|
||||
}
|
||||
|
||||
this.token = token;
|
||||
|
||||
+38
@@ -17,6 +17,8 @@
|
||||
|
||||
package org.keycloak.adapters;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.KeycloakPrincipal;
|
||||
import org.keycloak.adapters.spi.AuthChallenge;
|
||||
@@ -116,6 +118,12 @@ public abstract class RequestAuthenticator {
|
||||
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()) {
|
||||
log.trace("try oauth");
|
||||
}
|
||||
@@ -158,6 +166,36 @@ public abstract class RequestAuthenticator {
|
||||
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 BearerTokenRequestAuthenticator createBearerTokenAuthenticator() {
|
||||
|
||||
+83
-54
@@ -17,8 +17,16 @@
|
||||
*/
|
||||
package org.keycloak.adapters.authorization;
|
||||
|
||||
import java.net.URI;
|
||||
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.keycloak.AuthorizationContext;
|
||||
import org.keycloak.KeycloakSecurityContext;
|
||||
import org.keycloak.adapters.OIDCHttpFacade;
|
||||
import org.keycloak.adapters.spi.HttpFacade.Request;
|
||||
import org.keycloak.adapters.spi.HttpFacade.Response;
|
||||
@@ -31,13 +39,6 @@ import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.Enforce
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||
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>
|
||||
*/
|
||||
@@ -47,7 +48,7 @@ public abstract class AbstractPolicyEnforcer {
|
||||
private final PolicyEnforcerConfig enforcerConfig;
|
||||
private final PolicyEnforcer policyEnforcer;
|
||||
|
||||
private List<PathConfig> paths;
|
||||
private Map<String, PathConfig> paths;
|
||||
private AuthzClient authzClient;
|
||||
private PathMatcher pathMatcher;
|
||||
|
||||
@@ -56,7 +57,7 @@ public abstract class AbstractPolicyEnforcer {
|
||||
this.enforcerConfig = policyEnforcer.getEnforcerConfig();
|
||||
this.authzClient = policyEnforcer.getClient();
|
||||
this.pathMatcher = new PathMatcher();
|
||||
this.paths = new ArrayList<>(policyEnforcer.getPaths());
|
||||
this.paths = policyEnforcer.getPaths();
|
||||
}
|
||||
|
||||
public AuthorizationContext authorize(OIDCHttpFacade httpFacade) {
|
||||
@@ -66,40 +67,50 @@ public abstract class AbstractPolicyEnforcer {
|
||||
return createEmptyAuthorizationContext(true);
|
||||
}
|
||||
|
||||
AccessToken accessToken = httpFacade.getSecurityContext().getToken();
|
||||
Request request = httpFacade.getRequest();
|
||||
Response response = httpFacade.getResponse();
|
||||
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);
|
||||
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
|
||||
|
||||
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
|
||||
if (securityContext != null) {
|
||||
AccessToken accessToken = securityContext.getToken();
|
||||
|
||||
if (pathConfig == null) {
|
||||
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
|
||||
return createAuthorizationContext(accessToken);
|
||||
if (accessToken != null) {
|
||||
Request request = httpFacade.getRequest();
|
||||
Response response = httpFacade.getResponse();
|
||||
String path = getPath(request);
|
||||
PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
|
||||
|
||||
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
|
||||
|
||||
if (pathConfig == null) {
|
||||
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
|
||||
return createAuthorizationContext(accessToken);
|
||||
}
|
||||
|
||||
LOGGER.debugf("Could not find a configuration for path [%s]", path);
|
||||
response.sendError(403, "Could not find a configuration for path [" + path + "].");
|
||||
|
||||
return createEmptyAuthorizationContext(false);
|
||||
}
|
||||
|
||||
if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
|
||||
return createEmptyAuthorizationContext(true);
|
||||
}
|
||||
|
||||
PathConfig actualPathConfig = resolvePathConfig(pathConfig, request);
|
||||
Set<String> requiredScopes = getRequiredScopes(actualPathConfig, request);
|
||||
|
||||
if (isAuthorized(actualPathConfig, requiredScopes, accessToken, httpFacade)) {
|
||||
try {
|
||||
return createAuthorizationContext(accessToken);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error processing path [" + actualPathConfig.getPath() + "].", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!challenge(actualPathConfig, requiredScopes, httpFacade)) {
|
||||
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
|
||||
response.sendError(403, "Authorization failed.");
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.debugf("Could not find a configuration for path [%s]", path);
|
||||
response.sendError(403, "Could not find a configuration for path [" + path + "].");
|
||||
|
||||
return createEmptyAuthorizationContext(false);
|
||||
}
|
||||
|
||||
PathConfig actualPathConfig = resolvePathConfig(pathConfig, request);
|
||||
Set<String> requiredScopes = getRequiredScopes(actualPathConfig, request);
|
||||
|
||||
if (isAuthorized(actualPathConfig, requiredScopes, accessToken, httpFacade)) {
|
||||
try {
|
||||
return createAuthorizationContext(accessToken);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error processing path [" + actualPathConfig.getPath() + "].", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!challenge(actualPathConfig, requiredScopes, httpFacade)) {
|
||||
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
|
||||
response.sendError(403, "Authorization failed.");
|
||||
}
|
||||
|
||||
return createEmptyAuthorizationContext(false);
|
||||
@@ -110,12 +121,9 @@ public abstract class AbstractPolicyEnforcer {
|
||||
protected boolean isAuthorized(PathConfig actualPathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) {
|
||||
Request request = httpFacade.getRequest();
|
||||
PolicyEnforcerConfig enforcerConfig = getEnforcerConfig();
|
||||
String accessDeniedPath = enforcerConfig.getAccessDeniedPath();
|
||||
|
||||
if (accessDeniedPath != null) {
|
||||
if (request.getURI().contains(accessDeniedPath)) {
|
||||
return true;
|
||||
}
|
||||
if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
AccessToken.Authorization authorization = accessToken.getAuthorization();
|
||||
@@ -125,14 +133,17 @@ public abstract class AbstractPolicyEnforcer {
|
||||
}
|
||||
|
||||
List<Permission> permissions = authorization.getPermissions();
|
||||
boolean hasPermission = false;
|
||||
|
||||
for (Permission permission : permissions) {
|
||||
if (permission.getResourceSetId() != null) {
|
||||
if (isResourcePermission(actualPathConfig, permission)) {
|
||||
hasPermission = true;
|
||||
|
||||
if (actualPathConfig.isInstance() && !matchResourcePermission(actualPathConfig, permission)) {
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) {
|
||||
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
|
||||
if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) {
|
||||
@@ -143,16 +154,32 @@ public abstract class AbstractPolicyEnforcer {
|
||||
}
|
||||
} else {
|
||||
if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) {
|
||||
hasPermission = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPermission && EnforcementMode.PERMISSIVE.equals(actualPathConfig.getEnforcementMode())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LOGGER.debugf("Authorization FAILED for path [%s]. No enough permissions [%s].", actualPathConfig, permissions);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
Set<String> allowedScopes = permission.getScopes();
|
||||
return (allowedScopes.containsAll(requiredScopes) || allowedScopes.isEmpty());
|
||||
@@ -200,26 +227,23 @@ public abstract class AbstractPolicyEnforcer {
|
||||
}
|
||||
|
||||
private PathConfig resolvePathConfig(PathConfig originalConfig, Request request) {
|
||||
String path = getPath(request);
|
||||
|
||||
if (originalConfig.hasPattern()) {
|
||||
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();
|
||||
PathConfig config = PolicyEnforcer.createPathConfig(targetResource);
|
||||
|
||||
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);
|
||||
this.policyEnforcer.addPath(config);
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -228,6 +252,11 @@ public abstract class AbstractPolicyEnforcer {
|
||||
return originalConfig;
|
||||
}
|
||||
|
||||
private String getPath(Request request) {
|
||||
String pathInfo = URI.create(request.getURI()).getPath().substring(1);
|
||||
return pathInfo.substring(pathInfo.indexOf('/'), pathInfo.length());
|
||||
}
|
||||
|
||||
private Set<String> getRequiredScopes(PathConfig pathConfig, Request request) {
|
||||
Set<String> requiredScopes = new HashSet<>();
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer {
|
||||
|
||||
@Override
|
||||
protected boolean challenge(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) {
|
||||
if (getEnforcerConfig().getUmaProtocolConfig() != null) {
|
||||
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
||||
challengeUmaAuthentication(pathConfig, requiredScopes, facade);
|
||||
} else {
|
||||
challengeEntitlementAuthentication(facade);
|
||||
|
||||
+33
-36
@@ -17,6 +17,10 @@
|
||||
*/
|
||||
package org.keycloak.adapters.authorization;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
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.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>
|
||||
*/
|
||||
@@ -51,44 +51,39 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
||||
|
||||
@Override
|
||||
protected boolean isAuthorized(PathConfig pathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) {
|
||||
int retry = 2;
|
||||
AccessToken original = accessToken;
|
||||
|
||||
while (retry > 0) {
|
||||
if (super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
|
||||
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--;
|
||||
if (super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
protected boolean challenge(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) {
|
||||
String accessDeniedPath = getEnforcerConfig().getAccessDeniedPath();
|
||||
String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo();
|
||||
HttpFacade.Response response = facade.getResponse();
|
||||
|
||||
if (accessDeniedPath != null) {
|
||||
@@ -107,8 +102,8 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
||||
AuthzClient authzClient = getAuthzClient();
|
||||
KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
|
||||
|
||||
if (getEnforcerConfig().getUmaProtocolConfig() != null) {
|
||||
LOGGER.debug("Obtaining authorization for authenticated user.");
|
||||
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
||||
LOGGER.debug("Obtaining authorization for authenticated user.");
|
||||
PermissionRequest permissionRequest = new PermissionRequest();
|
||||
|
||||
permissionRequest.setResourceSetId(pathConfig.getId());
|
||||
@@ -136,12 +131,14 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
||||
permissionRequest.setResourceSetId(pathConfig.getId());
|
||||
permissionRequest.setResourceSetName(pathConfig.getName());
|
||||
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);
|
||||
EntitlementResponse authzResponse = authzClient.entitlement(accessToken).get(authzClient.getConfiguration().getClientId(), request);
|
||||
return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
|
||||
}
|
||||
}
|
||||
} catch (AuthorizationDeniedException e) {
|
||||
LOGGER.debug("Authorization denied", e);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unexpected error during authorization request.", e);
|
||||
|
||||
+9
-3
@@ -19,7 +19,7 @@ package org.keycloak.adapters.authorization;
|
||||
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
@@ -28,10 +28,16 @@ class PathMatcher {
|
||||
|
||||
private static final String ANY_RESOURCE_PATTERN = "/*";
|
||||
|
||||
PathConfig matches(final String requestedUri, List<PathConfig> paths) {
|
||||
PathConfig matches(final String requestedUri, Map<String, PathConfig> paths) {
|
||||
PathConfig pathConfig = paths.get(requestedUri);
|
||||
|
||||
if (pathConfig != null) {
|
||||
return pathConfig;
|
||||
}
|
||||
|
||||
PathConfig actualConfig = null;
|
||||
|
||||
for (PathConfig entry : paths) {
|
||||
for (PathConfig entry : paths.values()) {
|
||||
String protectedUri = entry.getPath();
|
||||
String selectedUri = null;
|
||||
|
||||
|
||||
+30
-14
@@ -34,8 +34,10 @@ import org.keycloak.representations.idm.authorization.Permission;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -48,7 +50,7 @@ public class PolicyEnforcer {
|
||||
private final KeycloakDeployment deployment;
|
||||
private final AuthzClient authzClient;
|
||||
private final PolicyEnforcerConfig enforcerConfig;
|
||||
private final List<PathConfig> paths;
|
||||
private final Map<String, PathConfig> paths;
|
||||
|
||||
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
|
||||
this.deployment = deployment;
|
||||
@@ -58,7 +60,7 @@ public class PolicyEnforcer {
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Initialization complete. Path configurations:");
|
||||
for (PathConfig pathConfig : this.paths) {
|
||||
for (PathConfig pathConfig : this.paths.values()) {
|
||||
LOGGER.debug(pathConfig);
|
||||
}
|
||||
}
|
||||
@@ -96,16 +98,29 @@ public class PolicyEnforcer {
|
||||
return authzClient;
|
||||
}
|
||||
|
||||
public List<PathConfig> getPaths() {
|
||||
return Collections.unmodifiableList(paths);
|
||||
public Map<String, PathConfig> getPaths() {
|
||||
return paths;
|
||||
}
|
||||
|
||||
void addPath(PathConfig pathConfig) {
|
||||
paths.put(pathConfig.getPath(), pathConfig);
|
||||
}
|
||||
|
||||
KeycloakDeployment getDeployment() {
|
||||
return deployment;
|
||||
}
|
||||
|
||||
private List<PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
||||
if (enforcerConfig.getPaths().isEmpty()) {
|
||||
private Map<String, PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
||||
boolean loadPathsFromServer = true;
|
||||
|
||||
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
|
||||
if (!PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
|
||||
loadPathsFromServer = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (loadPathsFromServer) {
|
||||
LOGGER.info("No path provided in configuration.");
|
||||
return configureAllPathsForResourceServer(protectedResource);
|
||||
} else {
|
||||
@@ -114,8 +129,8 @@ public class PolicyEnforcer {
|
||||
}
|
||||
}
|
||||
|
||||
private List<PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
||||
List<PathConfig> paths = new ArrayList<>();
|
||||
private Map<String, PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
||||
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
|
||||
|
||||
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
|
||||
Set<String> search;
|
||||
@@ -163,7 +178,7 @@ public class PolicyEnforcer {
|
||||
|
||||
PathConfig existingPath = null;
|
||||
|
||||
for (PathConfig current : paths) {
|
||||
for (PathConfig current : paths.values()) {
|
||||
if (current.getId().equals(pathConfig.getId()) && current.getPath().equals(pathConfig.getPath())) {
|
||||
existingPath = current;
|
||||
break;
|
||||
@@ -171,7 +186,7 @@ public class PolicyEnforcer {
|
||||
}
|
||||
|
||||
if (existingPath == null) {
|
||||
paths.add(pathConfig);
|
||||
paths.put(pathConfig.getPath(), pathConfig);
|
||||
} else {
|
||||
existingPath.getMethods().addAll(pathConfig.getMethods());
|
||||
existingPath.getScopes().addAll(pathConfig.getScopes());
|
||||
@@ -181,23 +196,24 @@ public class PolicyEnforcer {
|
||||
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.");
|
||||
List<PathConfig> paths = new ArrayList<>();
|
||||
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
|
||||
|
||||
for (String id : protectedResource.findAll()) {
|
||||
RegistrationResponse response = protectedResource.findById(id);
|
||||
ResourceRepresentation resourceDescription = response.getResourceDescription();
|
||||
|
||||
if (resourceDescription.getUri() != null) {
|
||||
paths.add(createPathConfig(resourceDescription));
|
||||
PathConfig pathConfig = createPathConfig(resourceDescription);
|
||||
paths.put(pathConfig.getPath(), pathConfig);
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
private PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
|
||||
static PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
|
||||
PathConfig pathConfig = new PathConfig();
|
||||
|
||||
pathConfig.setId(resourceDescription.getId());
|
||||
|
||||
+5
@@ -37,4 +37,9 @@ public class HardcodedPublicKeyLocator implements PublicKeyLocator {
|
||||
public PublicKey getPublicKey(String kid, KeycloakDeployment deployment) {
|
||||
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.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.util.JWKSUtils;
|
||||
|
||||
import java.security.PublicKey;
|
||||
@@ -48,36 +47,51 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
|
||||
@Override
|
||||
public PublicKey getPublicKey(String kid, KeycloakDeployment deployment) {
|
||||
int minTimeBetweenRequests = deployment.getMinTimeBetweenJwksRequests();
|
||||
int publicKeyCacheTtl = deployment.getPublicKeyCacheTtl();
|
||||
int currentTime = Time.currentTime();
|
||||
|
||||
// Check if key is in cache.
|
||||
PublicKey publicKey = currentKeys.get(kid);
|
||||
PublicKey publicKey = lookupCachedKey(publicKeyCacheTtl, currentTime, kid);
|
||||
if (publicKey != null) {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
int currentTime = Time.currentTime();
|
||||
|
||||
// Check if we are allowed to send request
|
||||
if (currentTime > lastRequestTime + minTimeBetweenRequests) {
|
||||
synchronized (this) {
|
||||
currentTime = Time.currentTime();
|
||||
if (currentTime > lastRequestTime + minTimeBetweenRequests) {
|
||||
sendRequest(deployment);
|
||||
lastRequestTime = currentTime;
|
||||
} else {
|
||||
log.debugf("Won't send request to realm jwks url. Last request time was %d", lastRequestTime);
|
||||
}
|
||||
synchronized (this) {
|
||||
currentTime = Time.currentTime();
|
||||
if (currentTime > lastRequestTime + minTimeBetweenRequests) {
|
||||
sendRequest(deployment);
|
||||
lastRequestTime = currentTime;
|
||||
} else {
|
||||
log.debug("Won't send request to realm jwks url. Last request time was " + 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) {
|
||||
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());
|
||||
@@ -87,7 +101,7 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
|
||||
Map<String, PublicKey> publicKeys = JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG);
|
||||
|
||||
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
|
||||
|
||||
+7
@@ -34,4 +34,11 @@ public interface PublicKeyLocator {
|
||||
*/
|
||||
PublicKey getPublicKey(String kid, KeycloakDeployment deployment);
|
||||
|
||||
/**
|
||||
* Reset the state of locator (eg. clear the cached keys)
|
||||
*
|
||||
* @param deployment
|
||||
*/
|
||||
void reset(KeycloakDeployment deployment);
|
||||
|
||||
}
|
||||
+2
@@ -69,6 +69,7 @@ public class KeycloakDeploymentBuilderTest {
|
||||
assertEquals("email", deployment.getPrincipalAttribute());
|
||||
assertEquals(10, deployment.getTokenMinimumTimeToLive());
|
||||
assertEquals(20, deployment.getMinTimeBetweenJwksRequests());
|
||||
assertEquals(120, deployment.getPublicKeyCacheTtl());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -78,6 +79,7 @@ public class KeycloakDeploymentBuilderTest {
|
||||
|
||||
assertTrue(deployment.getPublicKeyLocator() instanceof JWKPublicKeyLocator);
|
||||
assertEquals(10, deployment.getMinTimeBetweenJwksRequests());
|
||||
assertEquals(86400, deployment.getPublicKeyCacheTtl());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -30,5 +30,6 @@
|
||||
"token-store": "cookie",
|
||||
"principal-attribute": "email",
|
||||
"token-minimum-time-to-live": 10,
|
||||
"min-time-between-jwks-requests": 20
|
||||
"min-time-between-jwks-requests": 20,
|
||||
"public-key-cache-ttl": 120
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
+2
-2
@@ -86,7 +86,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
||||
|
||||
if (!SharedAttributeDefinitons.validateTruststoreSetIfRequired(addRealm)) {
|
||||
//TODO: externalize the message
|
||||
throw new XMLStreamException("truststore and truststore-password must be set if ssl-required is not none and disable-trust-maanger is false.");
|
||||
throw new XMLStreamException("truststore and truststore-password must be set if ssl-required is not none and disable-trust-manager is false.");
|
||||
}
|
||||
|
||||
list.add(addRealm);
|
||||
@@ -117,7 +117,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
||||
* TODO need to check realm-ref first.
|
||||
if (!SharedAttributeDefinitons.validateTruststoreSetIfRequired(addSecureDeployment)) {
|
||||
//TODO: externalize the message
|
||||
throw new XMLStreamException("truststore and truststore-password must be set if ssl-required is not none and disable-trust-maanger is false.");
|
||||
throw new XMLStreamException("truststore and truststore-password must be set if ssl-required is not none and disable-trust-manager is false.");
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ public final class RealmAddHandler extends AbstractAddStepHandler {
|
||||
|
||||
if (!SharedAttributeDefinitons.validateTruststoreSetIfRequired(model.clone())) {
|
||||
//TODO: externalize message
|
||||
throw new OperationFailedException("truststore and truststore-password must be set if ssl-required is not none and disable-trust-maanger is false.");
|
||||
throw new OperationFailedException("truststore and truststore-password must be set if ssl-required is not none and disable-trust-manager is false.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<name>Keycloak AS7 / JBoss EAP 6 Integration</name>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<name>Keycloak Jetty Integration</name>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -118,12 +118,11 @@
|
||||
/**
|
||||
* Obtains all entitlements from a Keycloak Server based on a give resourceServerId.
|
||||
*/
|
||||
this.entitlement = function (resourceSeververId) {
|
||||
this.entitlement = function (resourceSeververId, entitlementRequest ) {
|
||||
this.then = function (onGrant, onDeny, onError) {
|
||||
var request = new XMLHttpRequest();
|
||||
|
||||
request.open('GET', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/authz/entitlement/' + resourceSeververId, true);
|
||||
request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token)
|
||||
|
||||
|
||||
request.onreadystatechange = function () {
|
||||
if (request.readyState == 4) {
|
||||
@@ -149,7 +148,19 @@
|
||||
}
|
||||
};
|
||||
|
||||
request.send(null);
|
||||
var erJson = null
|
||||
|
||||
if(entitlementRequest) {
|
||||
request.open('POST', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/authz/entitlement/' + resourceSeververId, true);
|
||||
request.setRequestHeader("Content-type", "application/json");
|
||||
erJson = JSON.stringify(entitlementRequest)
|
||||
} else {
|
||||
request.open('GET', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/authz/entitlement/' + resourceSeververId, true);
|
||||
}
|
||||
|
||||
request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token)
|
||||
request.send(erJson);
|
||||
|
||||
};
|
||||
|
||||
return this;
|
||||
|
||||
@@ -210,11 +210,18 @@
|
||||
var nonce = createUUID();
|
||||
|
||||
var redirectUri = adapter.redirectUri(options);
|
||||
if (options && options.prompt) {
|
||||
redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'prompt=' + options.prompt;
|
||||
|
||||
var callbackState = {
|
||||
state: state,
|
||||
nonce: nonce,
|
||||
redirectUri: encodeURIComponent(redirectUri),
|
||||
}
|
||||
|
||||
callbackStorage.add({ state: state, nonce: nonce, redirectUri: encodeURIComponent(redirectUri) });
|
||||
if (options && options.prompt) {
|
||||
callbackState.prompt = options.prompt;
|
||||
}
|
||||
|
||||
callbackStorage.add(callbackState);
|
||||
|
||||
var action = 'auth';
|
||||
if (options && options.action == 'register') {
|
||||
@@ -747,6 +754,7 @@
|
||||
if (oauthState && (oauth.code || oauth.error || oauth.access_token || oauth.id_token)) {
|
||||
oauth.redirectUri = oauthState.redirectUri;
|
||||
oauth.storedNonce = oauthState.nonce;
|
||||
oauth.prompt = oauthState.prompt;
|
||||
|
||||
if (oauth.fragment) {
|
||||
oauth.newUrl += '#' + oauth.fragment;
|
||||
@@ -838,14 +846,15 @@
|
||||
kc.clearToken();
|
||||
}
|
||||
|
||||
for (var i = loginIframe.callbackList.length - 1; i >= 0; --i) {
|
||||
var promise = loginIframe.callbackList[i];
|
||||
var callbacks = loginIframe.callbackList.splice(0, loginIframe.callbackList.length);
|
||||
|
||||
for (var i = callbacks.length - 1; i >= 0; --i) {
|
||||
var promise = callbacks[i];
|
||||
if (event.data == "unchanged") {
|
||||
promise.setSuccess();
|
||||
} else {
|
||||
promise.setError();
|
||||
}
|
||||
loginIframe.callbackList.splice(i, 1);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1041,7 +1050,7 @@
|
||||
|
||||
function clearExpired() {
|
||||
var time = new Date().getTime();
|
||||
for (var i = 1; i <= localStorage.length; i++) {
|
||||
for (var i = 0; i < localStorage.length; i++) {
|
||||
var key = localStorage.key(i);
|
||||
if (key && key.indexOf('kc-callback-') == 0) {
|
||||
var value = localStorage.getItem(key);
|
||||
@@ -1218,9 +1227,6 @@
|
||||
case 'redirect_fragment':
|
||||
oauth.fragment = queryParams[param];
|
||||
break;
|
||||
case 'prompt':
|
||||
oauth.prompt = queryParams[param];
|
||||
break;
|
||||
default:
|
||||
if (responseMode != 'query' || !handleQueryParam(param, queryParams[param], oauth)) {
|
||||
oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + param + '=' + queryParams[param];
|
||||
|
||||
@@ -21,25 +21,28 @@
|
||||
var init;
|
||||
|
||||
function checkState(clientId, origin, sessionState, callback) {
|
||||
if (!init) {
|
||||
var cookie = getCookie();
|
||||
|
||||
if (!cookie) {
|
||||
callback('changed');
|
||||
} else if (!init) {
|
||||
var req = new XMLHttpRequest();
|
||||
|
||||
var url = location.href + "/init";
|
||||
url += "?client_id=" + encodeURIComponent(clientId);
|
||||
url += "&origin=" + encodeURIComponent(origin);
|
||||
url += "&session_state=" + encodeURIComponent(sessionState);
|
||||
|
||||
req.open('GET', url, true);
|
||||
|
||||
req.onreadystatechange = function () {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 204) {
|
||||
if (req.readyState === 4) {
|
||||
if (req.status === 204) {
|
||||
init = {
|
||||
clientId: clientId,
|
||||
origin: origin
|
||||
}
|
||||
callback('unchanged');
|
||||
} else if (req.status = 404) {
|
||||
} else if (req.status === 404) {
|
||||
callback('changed');
|
||||
} else {
|
||||
callback('error');
|
||||
@@ -49,9 +52,8 @@
|
||||
|
||||
req.send();
|
||||
} else {
|
||||
if (clientId == init.clientId && origin == init.origin) {
|
||||
var cookie = getCookie();
|
||||
if (sessionState == cookie) {
|
||||
if (clientId === init.clientId && origin === init.origin) {
|
||||
if (sessionState === cookie) {
|
||||
callback('unchanged');
|
||||
} else {
|
||||
callback('changed');
|
||||
@@ -69,7 +71,7 @@
|
||||
for(var i=0; i<ca.length; i++)
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -93,4 +95,4 @@
|
||||
window.addEventListener("message", receiveMessage, false);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<name>Keycloak OIDC Client Adapter Modules</name>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
+1
-1
@@ -215,7 +215,7 @@ public class KeycloakSpringBootConfiguration {
|
||||
securityHandler.setConstraintMappings(jettyConstraintMappings);
|
||||
securityHandler.setAuthenticator(keycloakJettyAuthenticator);
|
||||
|
||||
webAppContext.setHandler(securityHandler);
|
||||
webAppContext.setSecurityHandler(securityHandler);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
+12
-2
@@ -30,6 +30,12 @@ import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
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
|
||||
@@ -56,12 +62,15 @@ public class KeycloakAuthenticationEntryPoint implements AuthenticationEntryPoin
|
||||
private final RequestMatcher apiRequestMatcher;
|
||||
private String loginUri = DEFAULT_LOGIN_URI;
|
||||
private String realm = DEFAULT_REALM;
|
||||
|
||||
private AdapterDeploymentContext adapterDeploymentContext;
|
||||
|
||||
/**
|
||||
* Creates a new Keycloak authentication entry point.
|
||||
*/
|
||||
public KeycloakAuthenticationEntryPoint() {
|
||||
public KeycloakAuthenticationEntryPoint(AdapterDeploymentContext adapterDeploymentContext) {
|
||||
this(DEFAULT_API_REQUEST_MATCHER);
|
||||
this.adapterDeploymentContext = adapterDeploymentContext;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,7 +88,8 @@ public class KeycloakAuthenticationEntryPoint implements AuthenticationEntryPoin
|
||||
@Override
|
||||
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);
|
||||
} else {
|
||||
commenceLoginRedirect(request, response);
|
||||
|
||||
+2
-2
@@ -71,8 +71,8 @@ public abstract class KeycloakWebSecurityConfigurerAdapter extends WebSecurityCo
|
||||
return factoryBean.getObject();
|
||||
}
|
||||
|
||||
protected AuthenticationEntryPoint authenticationEntryPoint() {
|
||||
return new KeycloakAuthenticationEntryPoint();
|
||||
protected AuthenticationEntryPoint authenticationEntryPoint() throws Exception {
|
||||
return new KeycloakAuthenticationEntryPoint(adapterDeploymentContext());
|
||||
}
|
||||
|
||||
protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() {
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
+1
@@ -142,6 +142,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
|
||||
if (AuthOutcome.FAILED.equals(result)) {
|
||||
throw new KeycloakAuthenticationException("Auth outcome: " + result);
|
||||
}
|
||||
|
||||
else if (AuthOutcome.AUTHENTICATED.equals(result)) {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
Assert.notNull(authentication, "Authentication SecurityContextHolder was null");
|
||||
|
||||
+22
-1
@@ -26,6 +26,15 @@ import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
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.
|
||||
@@ -35,12 +44,24 @@ public class KeycloakAuthenticationEntryPointTest {
|
||||
private KeycloakAuthenticationEntryPoint authenticationEntryPoint;
|
||||
private MockHttpServletRequest request;
|
||||
private MockHttpServletResponse response;
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Mock
|
||||
private AdapterDeploymentContext adapterDeploymentContext;
|
||||
|
||||
@Mock
|
||||
private KeycloakDeployment keycloakDeployment;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
authenticationEntryPoint = new KeycloakAuthenticationEntryPoint();
|
||||
MockitoAnnotations.initMocks(this);
|
||||
authenticationEntryPoint = new KeycloakAuthenticationEntryPoint(adapterDeploymentContext);
|
||||
request = new MockHttpServletRequest();
|
||||
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
|
||||
|
||||
+1
@@ -154,6 +154,7 @@ public class KeycloakAuthenticationProcessingFilterTest {
|
||||
when(keycloakDeployment.getResourceName()).thenReturn("resource-name");
|
||||
when(keycloakDeployment.getStateCookieName()).thenReturn("kc-cookie");
|
||||
when(keycloakDeployment.getSslRequired()).thenReturn(SslRequired.NONE);
|
||||
when(keycloakDeployment.isBearerOnly()).thenReturn(Boolean.FALSE);
|
||||
filter.attemptAuthentication(request, response);
|
||||
|
||||
verify(response).setStatus(302);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<name>Keycloak Tomcat Integration</name>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
+3
-1
@@ -127,7 +127,7 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat
|
||||
InputStream configInputStream = getConfigInputStream(context);
|
||||
KeycloakDeployment kd;
|
||||
if (configInputStream == null) {
|
||||
log.fine("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
|
||||
log.warning("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
|
||||
kd = new KeycloakDeployment();
|
||||
} else {
|
||||
kd = KeycloakDeploymentBuilder.build(configInputStream);
|
||||
@@ -196,6 +196,8 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat
|
||||
CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, response);
|
||||
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
|
||||
if (deployment == null || !deployment.isConfigured()) {
|
||||
//needed for the EAP6/AS7 adapter relying on the tomcat core adapter
|
||||
facade.getResponse().sendError(401);
|
||||
return false;
|
||||
}
|
||||
AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
+11
@@ -39,9 +39,20 @@ import java.util.List;
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class KeycloakAuthenticatorValve extends AbstractKeycloakAuthenticatorValve {
|
||||
|
||||
/**
|
||||
* Method called by Tomcat < 8.5.5
|
||||
*/
|
||||
public boolean authenticate(Request request, HttpServletResponse response) throws IOException {
|
||||
return authenticateInternal(request, response, request.getContext().getLoginConfig());
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called by Tomcat >= 8.5.5
|
||||
*/
|
||||
protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException {
|
||||
return this.authenticate(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
+1
-1
@@ -92,7 +92,7 @@ public abstract class AbstractUndertowKeycloakAuthMech implements Authentication
|
||||
UndertowHttpFacade facade = createFacade(exchange);
|
||||
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
|
||||
KeycloakSecurityContext ksc = exchange.getAttachment(OIDCUndertowHttpFacade.KEYCLOAK_SECURITY_CONTEXT_KEY);
|
||||
if (ksc != null && ksc instanceof RefreshableKeycloakSecurityContext) {
|
||||
if (!deployment.isBearerOnly() && ksc != null && ksc instanceof RefreshableKeycloakSecurityContext) {
|
||||
((RefreshableKeycloakSecurityContext) ksc).logout(deployment);
|
||||
}
|
||||
AdapterTokenStore tokenStore = getTokenStore(exchange, facade, deployment, securityContext);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<name>Keycloak WildFly Integration</name>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
+2
-2
@@ -87,7 +87,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
||||
|
||||
if (!SharedAttributeDefinitons.validateTruststoreSetIfRequired(addRealm)) {
|
||||
//TODO: externalize the message
|
||||
throw new XMLStreamException("truststore and truststore-password must be set if ssl-required is not none and disable-trust-maanger is false.");
|
||||
throw new XMLStreamException("truststore and truststore-password must be set if ssl-required is not none and disable-trust-manager is false.");
|
||||
}
|
||||
|
||||
list.add(addRealm);
|
||||
@@ -118,7 +118,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
||||
* TODO need to check realm-ref first.
|
||||
if (!SharedAttributeDefinitons.validateTruststoreSetIfRequired(addSecureDeployment)) {
|
||||
//TODO: externalize the message
|
||||
throw new XMLStreamException("truststore and truststore-password must be set if ssl-required is not none and disable-trust-maanger is false.");
|
||||
throw new XMLStreamException("truststore and truststore-password must be set if ssl-required is not none and disable-trust-manager is false.");
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ public final class RealmAddHandler extends AbstractAddStepHandler {
|
||||
|
||||
if (!SharedAttributeDefinitons.validateTruststoreSetIfRequired(model.clone())) {
|
||||
//TODO: externalize message
|
||||
throw new OperationFailedException("truststore and truststore-password must be set if ssl-required is not none and disable-trust-maanger is false.");
|
||||
throw new OperationFailedException("truststore and truststore-password must be set if ssl-required is not none and disable-trust-manager is false.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
+2
-2
@@ -20,10 +20,10 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<name>Keycloak Integration</name>
|
||||
<name>Keycloak Adapters</name>
|
||||
<description/>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-saml-eap-integration-pom</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<name>Keycloak SAML EAP Integration</name>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-saml-eap-integration-pom</artifactId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ public class SamlPrincipal implements Serializable, Principal {
|
||||
* @return
|
||||
*/
|
||||
public List<String> getFriendlyAttributes(String friendlyName) {
|
||||
List<String> list = friendlyAttributes.get(name);
|
||||
List<String> list = friendlyAttributes.get(friendlyName);
|
||||
if (list != null) {
|
||||
return Collections.unmodifiableList(list);
|
||||
} else {
|
||||
|
||||
Whitespace-only changes.
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>2.3.0.Final</version>
|
||||
<version>2.5.0.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -34,6 +34,7 @@
|
||||
<timestamp>${maven.build.timestamp}</timestamp>
|
||||
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
@@ -70,6 +71,11 @@
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
+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.cloned;
|
||||
|
||||
/**
|
||||
* Configuration options relevant for configuring http client that can be used by adapter.
|
||||
*
|
||||
* NOTE: keep in sync with core/src/main/java/org/keycloak/representations/adapters/config/AdapterHttpClientConfig.java until unified.
|
||||
*
|
||||
* @author hmlnarik
|
||||
*/
|
||||
public interface AdapterHttpClientConfig {
|
||||
|
||||
/**
|
||||
* Returns truststore filename.
|
||||
*/
|
||||
public String getTruststore();
|
||||
|
||||
/**
|
||||
* Returns truststore password.
|
||||
*/
|
||||
public String getTruststorePassword();
|
||||
|
||||
/**
|
||||
* Returns keystore with client keys.
|
||||
*/
|
||||
public String getClientKeystore();
|
||||
|
||||
/**
|
||||
* Returns keystore password.
|
||||
*/
|
||||
public String getClientKeystorePassword();
|
||||
|
||||
/**
|
||||
* Returns boolean flag whether any hostname verification is done on the server's
|
||||
* certificate, {@code true} means that verification is not done.
|
||||
* @return
|
||||
*/
|
||||
public boolean isAllowAnyHostname();
|
||||
|
||||
/**
|
||||
* Returns boolean flag whether any trust management and hostname verification is done.
|
||||
* <p>
|
||||
* <i>NOTE</i> Disabling trust manager is a security hole, so only set this option
|
||||
* if you cannot or do not want to verify the identity of the
|
||||
* host you are communicating with.
|
||||
*/
|
||||
public boolean isDisableTrustManager();
|
||||
|
||||
/**
|
||||
* Returns size of connection pool.
|
||||
*/
|
||||
public int getConnectionPoolSize();
|
||||
|
||||
/**
|
||||
* Returns URL of HTTP proxy.
|
||||
*/
|
||||
public String getProxyUrl();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.cloned;
|
||||
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.keycloak.adapters.saml.descriptor.parsers.SamlDescriptorIDPKeysExtractor;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:hmlnarik@redhat.com">Hynek Mlnařík</a>
|
||||
*/
|
||||
public class HttpAdapterUtils {
|
||||
|
||||
public static MultivaluedHashMap<String, KeyInfo> downloadKeysFromSamlDescriptor(HttpClient client, String descriptorUrl) throws HttpClientAdapterException {
|
||||
try {
|
||||
HttpGet httpRequest = new HttpGet(descriptorUrl);
|
||||
HttpResponse response = client.execute(httpRequest);
|
||||
int status = response.getStatusLine().getStatusCode();
|
||||
if (status != HttpStatus.SC_OK) {
|
||||
EntityUtils.consumeQuietly(response.getEntity());
|
||||
throw new HttpClientAdapterException("Unexpected status = " + status);
|
||||
}
|
||||
|
||||
HttpEntity entity = response.getEntity();
|
||||
if (entity == null) {
|
||||
throw new HttpClientAdapterException("There was no entity.");
|
||||
}
|
||||
|
||||
MultivaluedHashMap<String, KeyInfo> res;
|
||||
try (InputStream is = entity.getContent()) {
|
||||
res = extractKeysFromSamlDescriptor(is);
|
||||
}
|
||||
|
||||
EntityUtils.consumeQuietly(entity);
|
||||
|
||||
return res;
|
||||
} catch (IOException | ParsingException e) {
|
||||
throw new HttpClientAdapterException("IO error", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses SAML descriptor and extracts keys from it.
|
||||
* @param xmlStream
|
||||
* @return List of KeyInfo objects containing keys from the descriptor.
|
||||
* @throws IOException
|
||||
*/
|
||||
public static MultivaluedHashMap<String, KeyInfo> extractKeysFromSamlDescriptor(InputStream xmlStream) throws ParsingException {
|
||||
Object res = new SamlDescriptorIDPKeysExtractor().parse(xmlStream);
|
||||
return (MultivaluedHashMap<String, KeyInfo>) res;
|
||||
}
|
||||
|
||||
}
|
||||
+5
-20
@@ -15,33 +15,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.mappers;
|
||||
package org.keycloak.adapters.cloned;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class FederationConfigValidationException extends Exception {
|
||||
public class HttpClientAdapterException extends Exception {
|
||||
|
||||
private Object[] parameters;
|
||||
|
||||
public FederationConfigValidationException(String message) {
|
||||
public HttpClientAdapterException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FederationConfigValidationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public FederationConfigValidationException(String message, Object ... parameters) {
|
||||
super(message);
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
public Object[] getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public void setParameters(Object[] parameters) {
|
||||
this.parameters = parameters;
|
||||
public HttpClientAdapterException(String message, Throwable t) {
|
||||
super(message, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
/*
|
||||
* 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.cloned;
|
||||
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.client.CookieStore;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.conn.ClientConnectionManager;
|
||||
import org.apache.http.conn.params.ConnRoutePNames;
|
||||
import org.apache.http.conn.scheme.PlainSocketFactory;
|
||||
import org.apache.http.conn.scheme.Scheme;
|
||||
import org.apache.http.conn.scheme.SchemeRegistry;
|
||||
import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
|
||||
import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
|
||||
import org.apache.http.conn.ssl.SSLSocketFactory;
|
||||
import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
||||
import org.apache.http.conn.ssl.X509HostnameVerifier;
|
||||
import org.apache.http.cookie.Cookie;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.apache.http.impl.conn.SingleClientConnManager;
|
||||
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
|
||||
import org.apache.http.params.BasicHttpParams;
|
||||
import org.apache.http.params.HttpConnectionParams;
|
||||
import org.keycloak.common.util.EnvUtil;
|
||||
import org.keycloak.common.util.KeystoreUtil;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Abstraction for creating HttpClients. Allows SSL configuration.
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class HttpClientBuilder {
|
||||
public static enum HostnameVerificationPolicy {
|
||||
/**
|
||||
* Hostname verification is not done on the server's certificate
|
||||
*/
|
||||
ANY,
|
||||
/**
|
||||
* Allows wildcards in subdomain names i.e. *.foo.com
|
||||
*/
|
||||
WILDCARD,
|
||||
/**
|
||||
* CN must match hostname connecting to
|
||||
*/
|
||||
STRICT
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
private static class PassthroughTrustManager implements X509TrustManager {
|
||||
public void checkClientTrusted(X509Certificate[] chain,
|
||||
String authType) throws CertificateException {
|
||||
}
|
||||
|
||||
public void checkServerTrusted(X509Certificate[] chain,
|
||||
String authType) throws CertificateException {
|
||||
}
|
||||
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected KeyStore truststore;
|
||||
protected KeyStore clientKeyStore;
|
||||
protected String clientPrivateKeyPassword;
|
||||
protected boolean disableTrustManager;
|
||||
protected boolean disableCookieCache = true;
|
||||
protected HostnameVerificationPolicy policy = HostnameVerificationPolicy.WILDCARD;
|
||||
protected SSLContext sslContext;
|
||||
protected int connectionPoolSize = 100;
|
||||
protected int maxPooledPerRoute = 0;
|
||||
protected long connectionTTL = -1;
|
||||
protected TimeUnit connectionTTLUnit = TimeUnit.MILLISECONDS;
|
||||
protected HostnameVerifier verifier = null;
|
||||
protected long socketTimeout = -1;
|
||||
protected TimeUnit socketTimeoutUnits = TimeUnit.MILLISECONDS;
|
||||
protected long establishConnectionTimeout = -1;
|
||||
protected TimeUnit establishConnectionTimeoutUnits = TimeUnit.MILLISECONDS;
|
||||
protected HttpHost proxyHost;
|
||||
|
||||
|
||||
/**
|
||||
* Socket inactivity timeout
|
||||
*
|
||||
* @param timeout
|
||||
* @param unit
|
||||
* @return
|
||||
*/
|
||||
public HttpClientBuilder socketTimeout(long timeout, TimeUnit unit) {
|
||||
this.socketTimeout = timeout;
|
||||
this.socketTimeoutUnits = unit;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* When trying to make an initial socket connection, what is the timeout?
|
||||
*
|
||||
* @param timeout
|
||||
* @param unit
|
||||
* @return
|
||||
*/
|
||||
public HttpClientBuilder establishConnectionTimeout(long timeout, TimeUnit unit) {
|
||||
this.establishConnectionTimeout = timeout;
|
||||
this.establishConnectionTimeoutUnits = unit;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpClientBuilder connectionTTL(long ttl, TimeUnit unit) {
|
||||
this.connectionTTL = ttl;
|
||||
this.connectionTTLUnit = unit;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpClientBuilder maxPooledPerRoute(int maxPooledPerRoute) {
|
||||
this.maxPooledPerRoute = maxPooledPerRoute;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpClientBuilder connectionPoolSize(int connectionPoolSize) {
|
||||
this.connectionPoolSize = connectionPoolSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable trust management and hostname verification. <i>NOTE</i> this is a security
|
||||
* hole, so only set this option if you cannot or do not want to verify the identity of the
|
||||
* host you are communicating with.
|
||||
*/
|
||||
public HttpClientBuilder disableTrustManager() {
|
||||
this.disableTrustManager = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpClientBuilder disableCookieCache() {
|
||||
this.disableCookieCache = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSL policy used to verify hostnames
|
||||
*
|
||||
* @param policy
|
||||
* @return
|
||||
*/
|
||||
public HttpClientBuilder hostnameVerification(HostnameVerificationPolicy policy) {
|
||||
this.policy = policy;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public HttpClientBuilder sslContext(SSLContext sslContext) {
|
||||
this.sslContext = sslContext;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpClientBuilder trustStore(KeyStore truststore) {
|
||||
this.truststore = truststore;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpClientBuilder keyStore(KeyStore keyStore, String password) {
|
||||
this.clientKeyStore = keyStore;
|
||||
this.clientPrivateKeyPassword = password;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpClientBuilder keyStore(KeyStore keyStore, char[] password) {
|
||||
this.clientKeyStore = keyStore;
|
||||
this.clientPrivateKeyPassword = new String(password);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
static class VerifierWrapper implements X509HostnameVerifier {
|
||||
protected HostnameVerifier verifier;
|
||||
|
||||
VerifierWrapper(HostnameVerifier verifier) {
|
||||
this.verifier = verifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(String host, SSLSocket ssl) throws IOException {
|
||||
if (!verifier.verify(host, ssl.getSession())) throw new SSLException("Hostname verification failure");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(String host, X509Certificate cert) throws SSLException {
|
||||
throw new SSLException("This verification path not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException {
|
||||
throw new SSLException("This verification path not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(String s, SSLSession sslSession) {
|
||||
return verifier.verify(s, sslSession);
|
||||
}
|
||||
}
|
||||
|
||||
public HttpClient build() {
|
||||
X509HostnameVerifier verifier = null;
|
||||
if (this.verifier != null) verifier = new VerifierWrapper(this.verifier);
|
||||
else {
|
||||
switch (policy) {
|
||||
case ANY:
|
||||
verifier = new AllowAllHostnameVerifier();
|
||||
break;
|
||||
case WILDCARD:
|
||||
verifier = new BrowserCompatHostnameVerifier();
|
||||
break;
|
||||
case STRICT:
|
||||
verifier = new StrictHostnameVerifier();
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
SSLSocketFactory sslsf = null;
|
||||
SSLContext theContext = sslContext;
|
||||
if (disableTrustManager) {
|
||||
theContext = SSLContext.getInstance("SSL");
|
||||
theContext.init(null, new TrustManager[]{new PassthroughTrustManager()},
|
||||
new SecureRandom());
|
||||
verifier = new AllowAllHostnameVerifier();
|
||||
sslsf = new SniSSLSocketFactory(theContext, verifier);
|
||||
} else if (theContext != null) {
|
||||
sslsf = new SniSSLSocketFactory(theContext, verifier);
|
||||
} else if (clientKeyStore != null || truststore != null) {
|
||||
sslsf = new SniSSLSocketFactory(SSLSocketFactory.TLS, clientKeyStore, clientPrivateKeyPassword, truststore, null, verifier);
|
||||
} else {
|
||||
final SSLContext tlsContext = SSLContext.getInstance(SSLSocketFactory.TLS);
|
||||
tlsContext.init(null, null, null);
|
||||
sslsf = new SniSSLSocketFactory(tlsContext, verifier);
|
||||
}
|
||||
SchemeRegistry registry = new SchemeRegistry();
|
||||
registry.register(
|
||||
new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
|
||||
Scheme httpsScheme = new Scheme("https", 443, sslsf);
|
||||
registry.register(httpsScheme);
|
||||
ClientConnectionManager cm = null;
|
||||
if (connectionPoolSize > 0) {
|
||||
ThreadSafeClientConnManager tcm = new ThreadSafeClientConnManager(registry, connectionTTL, connectionTTLUnit);
|
||||
tcm.setMaxTotal(connectionPoolSize);
|
||||
if (maxPooledPerRoute == 0) maxPooledPerRoute = connectionPoolSize;
|
||||
tcm.setDefaultMaxPerRoute(maxPooledPerRoute);
|
||||
cm = tcm;
|
||||
|
||||
} else {
|
||||
cm = new SingleClientConnManager(registry);
|
||||
}
|
||||
BasicHttpParams params = new BasicHttpParams();
|
||||
|
||||
if (proxyHost != null) {
|
||||
params.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxyHost);
|
||||
}
|
||||
|
||||
if (socketTimeout > -1) {
|
||||
HttpConnectionParams.setSoTimeout(params, (int) socketTimeoutUnits.toMillis(socketTimeout));
|
||||
|
||||
}
|
||||
if (establishConnectionTimeout > -1) {
|
||||
HttpConnectionParams.setConnectionTimeout(params, (int) establishConnectionTimeoutUnits.toMillis(establishConnectionTimeout));
|
||||
}
|
||||
DefaultHttpClient client = new DefaultHttpClient(cm, params);
|
||||
|
||||
if (disableCookieCache) {
|
||||
client.setCookieStore(new CookieStore() {
|
||||
@Override
|
||||
public void addCookie(Cookie cookie) {
|
||||
//To change body of implemented methods use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Cookie> getCookies() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clearExpired(Date date) {
|
||||
return false; //To change body of implemented methods use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
//To change body of implemented methods use File | Settings | File Templates.
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
return client;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public HttpClient build(AdapterHttpClientConfig adapterConfig) {
|
||||
disableCookieCache(); // disable cookie cache as we don't want sticky sessions for load balancing
|
||||
|
||||
String truststorePath = adapterConfig.getTruststore();
|
||||
if (truststorePath != null) {
|
||||
truststorePath = EnvUtil.replace(truststorePath);
|
||||
String truststorePassword = adapterConfig.getTruststorePassword();
|
||||
try {
|
||||
this.truststore = KeystoreUtil.loadKeyStore(truststorePath, truststorePassword);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load truststore", e);
|
||||
}
|
||||
}
|
||||
String clientKeystore = adapterConfig.getClientKeystore();
|
||||
if (clientKeystore != null) {
|
||||
clientKeystore = EnvUtil.replace(clientKeystore);
|
||||
String clientKeystorePassword = adapterConfig.getClientKeystorePassword();
|
||||
try {
|
||||
KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword);
|
||||
keyStore(clientCertKeystore, clientKeystorePassword);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load keystore", e);
|
||||
}
|
||||
}
|
||||
int size = 10;
|
||||
if (adapterConfig.getConnectionPoolSize() > 0)
|
||||
size = adapterConfig.getConnectionPoolSize();
|
||||
HttpClientBuilder.HostnameVerificationPolicy policy = HttpClientBuilder.HostnameVerificationPolicy.WILDCARD;
|
||||
if (adapterConfig.isAllowAnyHostname())
|
||||
policy = HttpClientBuilder.HostnameVerificationPolicy.ANY;
|
||||
connectionPoolSize(size);
|
||||
hostnameVerification(policy);
|
||||
if (adapterConfig.isDisableTrustManager()) {
|
||||
disableTrustManager();
|
||||
} else {
|
||||
trustStore(truststore);
|
||||
}
|
||||
|
||||
configureProxyForAuthServerIfProvided(adapterConfig);
|
||||
|
||||
return build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures a the proxy to use for auth-server requests if provided.
|
||||
* <p>
|
||||
* If the given {@link AdapterHttpClientConfig} contains the attribute {@code proxy-url} we use the
|
||||
* given URL as a proxy server, otherwise the proxy configuration is ignored.
|
||||
* </p>
|
||||
*
|
||||
* @param adapterConfig
|
||||
*/
|
||||
private void configureProxyForAuthServerIfProvided(AdapterHttpClientConfig adapterConfig) {
|
||||
|
||||
if (adapterConfig == null || adapterConfig.getProxyUrl() == null || adapterConfig.getProxyUrl().trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
URI uri = URI.create(adapterConfig.getProxyUrl());
|
||||
this.proxyHost = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.cloned;
|
||||
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.conn.scheme.HostNameResolver;
|
||||
import org.apache.http.conn.ssl.SSLSocketFactory;
|
||||
import org.apache.http.conn.ssl.TrustStrategy;
|
||||
import org.apache.http.conn.ssl.X509HostnameVerifier;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.security.AccessController;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivilegedActionException;
|
||||
import java.security.PrivilegedExceptionAction;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* SSLSocketFactory that uses Server Name Indication (SNI) TLS extension.
|
||||
*
|
||||
* <p>
|
||||
* Originally copied from <b>keycloak-adapter-core</b> project.
|
||||
*
|
||||
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||
* @author <a href="mailto:hmlnarik@redhat.com">Hynek Mlnařík</a>
|
||||
*/
|
||||
public class SniSSLSocketFactory extends SSLSocketFactory {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(SniSSLSocketFactory.class.getName());
|
||||
|
||||
public SniSSLSocketFactory(String algorithm, KeyStore keystore, String keyPassword, KeyStore truststore, SecureRandom random, HostNameResolver nameResolver) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||
super(algorithm, keystore, keyPassword, truststore, random, nameResolver);
|
||||
}
|
||||
|
||||
public SniSSLSocketFactory(String algorithm, KeyStore keystore, String keyPassword, KeyStore truststore, SecureRandom random, TrustStrategy trustStrategy, X509HostnameVerifier hostnameVerifier) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||
super(algorithm, keystore, keyPassword, truststore, random, trustStrategy, hostnameVerifier);
|
||||
}
|
||||
|
||||
public SniSSLSocketFactory(String algorithm, KeyStore keystore, String keyPassword, KeyStore truststore, SecureRandom random, X509HostnameVerifier hostnameVerifier) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||
super(algorithm, keystore, keyPassword, truststore, random, hostnameVerifier);
|
||||
}
|
||||
|
||||
public SniSSLSocketFactory(KeyStore keystore, String keystorePassword, KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||
super(keystore, keystorePassword, truststore);
|
||||
}
|
||||
|
||||
public SniSSLSocketFactory(KeyStore keystore, String keystorePassword) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||
super(keystore, keystorePassword);
|
||||
}
|
||||
|
||||
public SniSSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||
super(truststore);
|
||||
}
|
||||
|
||||
public SniSSLSocketFactory(TrustStrategy trustStrategy, X509HostnameVerifier hostnameVerifier) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||
super(trustStrategy, hostnameVerifier);
|
||||
}
|
||||
|
||||
public SniSSLSocketFactory(TrustStrategy trustStrategy) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||
super(trustStrategy);
|
||||
}
|
||||
|
||||
public SniSSLSocketFactory(SSLContext sslContext) {
|
||||
super(sslContext);
|
||||
}
|
||||
|
||||
public SniSSLSocketFactory(SSLContext sslContext, HostNameResolver nameResolver) {
|
||||
super(sslContext, nameResolver);
|
||||
}
|
||||
|
||||
public SniSSLSocketFactory(SSLContext sslContext, X509HostnameVerifier hostnameVerifier) {
|
||||
super(sslContext, hostnameVerifier);
|
||||
}
|
||||
|
||||
public SniSSLSocketFactory(SSLContext sslContext, String[] supportedProtocols, String[] supportedCipherSuites, X509HostnameVerifier hostnameVerifier) {
|
||||
super(sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier);
|
||||
}
|
||||
|
||||
public SniSSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory, X509HostnameVerifier hostnameVerifier) {
|
||||
super(socketfactory, hostnameVerifier);
|
||||
}
|
||||
|
||||
public SniSSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory, String[] supportedProtocols, String[] supportedCipherSuites, X509HostnameVerifier hostnameVerifier) {
|
||||
super(socketfactory, supportedProtocols, supportedCipherSuites, hostnameVerifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpContext context) throws IOException {
|
||||
return super.connectSocket(connectTimeout, applySNI(socket, host.getHostName()), host, remoteAddress, localAddress, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createLayeredSocket(Socket socket, String target, int port, HttpContext context) throws IOException {
|
||||
return super.createLayeredSocket(applySNI(socket, target), target, port, context);
|
||||
}
|
||||
|
||||
private Socket applySNI(final Socket socket, String hostname) {
|
||||
if (socket instanceof SSLSocket) {
|
||||
try {
|
||||
Method setHostMethod = AccessController.doPrivileged(new PrivilegedExceptionAction<Method>() {
|
||||
@Override
|
||||
public Method run() throws NoSuchMethodException {
|
||||
return socket.getClass().getMethod("setHost", String.class);
|
||||
}
|
||||
});
|
||||
|
||||
setHostMethod.invoke(socket, hostname);
|
||||
LOG.log(Level.FINEST, "Applied SNI to socket for host {0}", hostname);
|
||||
} catch (PrivilegedActionException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||
LOG.log(Level.WARNING, "Failed to apply SNI to SSLSocket", e);
|
||||
}
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -79,7 +79,9 @@ public abstract class AbstractInitiateLogin implements AuthChallenge {
|
||||
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
|
||||
}
|
||||
|
||||
binding.signWith(keypair);
|
||||
binding.signWith(null, keypair);
|
||||
// TODO: As part of KEYCLOAK-3810, add KeyID to the SAML document
|
||||
// <related DocumentBuilder>.addExtension(new KeycloakKeySamlExtensionGenerator(<key ID>));
|
||||
binding.signDocument();
|
||||
}
|
||||
return binding;
|
||||
|
||||
+51
-5
@@ -23,7 +23,14 @@ import org.keycloak.saml.SignatureAlgorithm;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.keycloak.adapters.saml.rotation.SamlDescriptorPublicKeyLocator;
|
||||
import org.keycloak.rotation.CompositeKeyLocator;
|
||||
import org.keycloak.rotation.HardcodedKeyLocator;
|
||||
import org.keycloak.rotation.KeyLocator;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
@@ -179,10 +186,15 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||
|
||||
public static class DefaultIDP implements IDP {
|
||||
|
||||
private static final int DEFAULT_CACHE_TTL = 24 * 60 * 60;
|
||||
|
||||
private String entityID;
|
||||
private PublicKey signatureValidationKey;
|
||||
private final CompositeKeyLocator signatureValidationKeyLocator = new CompositeKeyLocator();
|
||||
private SingleSignOnService singleSignOnService;
|
||||
private SingleLogoutService singleLogoutService;
|
||||
private final List<PublicKey> signatureValidationKeys = new LinkedList<>();
|
||||
private int minTimeBetweenDescriptorRequests;
|
||||
private HttpClient client;
|
||||
|
||||
@Override
|
||||
public String getEntityID() {
|
||||
@@ -200,16 +212,25 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey getSignatureValidationKey() {
|
||||
return signatureValidationKey;
|
||||
public KeyLocator getSignatureValidationKeyLocator() {
|
||||
return this.signatureValidationKeyLocator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinTimeBetweenDescriptorRequests() {
|
||||
return minTimeBetweenDescriptorRequests;
|
||||
}
|
||||
|
||||
public void setMinTimeBetweenDescriptorRequests(int minTimeBetweenDescriptorRequests) {
|
||||
this.minTimeBetweenDescriptorRequests = minTimeBetweenDescriptorRequests;
|
||||
}
|
||||
|
||||
public void setEntityID(String entityID) {
|
||||
this.entityID = entityID;
|
||||
}
|
||||
|
||||
public void setSignatureValidationKey(PublicKey signatureValidationKey) {
|
||||
this.signatureValidationKey = signatureValidationKey;
|
||||
public void addSignatureValidationKey(PublicKey signatureValidationKey) {
|
||||
this.signatureValidationKeys.add(signatureValidationKey);
|
||||
}
|
||||
|
||||
public void setSingleSignOnService(SingleSignOnService singleSignOnService) {
|
||||
@@ -219,6 +240,31 @@ public class DefaultSamlDeployment implements SamlDeployment {
|
||||
public void setSingleLogoutService(SingleLogoutService singleLogoutService) {
|
||||
this.singleLogoutService = singleLogoutService;
|
||||
}
|
||||
|
||||
public void refreshKeyLocatorConfiguration() {
|
||||
this.signatureValidationKeyLocator.clear();
|
||||
|
||||
// When key is set, use that (and only that), otherwise configure dynamic key locator
|
||||
if (! this.signatureValidationKeys.isEmpty()) {
|
||||
this.signatureValidationKeyLocator.add(new HardcodedKeyLocator(this.signatureValidationKeys));
|
||||
} else if (this.singleSignOnService != null) {
|
||||
String samlDescriptorUrl = singleSignOnService.getRequestBindingUrl() + "/descriptor";
|
||||
HttpClient httpClient = getClient();
|
||||
SamlDescriptorPublicKeyLocator samlDescriptorPublicKeyLocator =
|
||||
new SamlDescriptorPublicKeyLocator(
|
||||
samlDescriptorUrl, this.minTimeBetweenDescriptorRequests, DEFAULT_CACHE_TTL, httpClient);
|
||||
this.signatureValidationKeyLocator.add(samlDescriptorPublicKeyLocator);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpClient getClient() {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public void setClient(HttpClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
}
|
||||
|
||||
private IDP idp;
|
||||
|
||||
@@ -22,14 +22,18 @@ import org.keycloak.saml.SignatureAlgorithm;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Set;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.keycloak.rotation.KeyLocator;
|
||||
|
||||
/**
|
||||
* Represents SAML deployment configuration.
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface SamlDeployment {
|
||||
|
||||
enum Binding {
|
||||
POST,
|
||||
REDIRECT;
|
||||
@@ -41,20 +45,68 @@ public interface SamlDeployment {
|
||||
}
|
||||
|
||||
public interface IDP {
|
||||
/**
|
||||
* Returns entity identifier of this IdP.
|
||||
* @return see description.
|
||||
*/
|
||||
String getEntityID();
|
||||
|
||||
/**
|
||||
* Returns Single sign on service configuration for this IdP.
|
||||
* @return see description.
|
||||
*/
|
||||
SingleSignOnService getSingleSignOnService();
|
||||
|
||||
/**
|
||||
* Returns Single logout service configuration for this IdP.
|
||||
* @return see description.
|
||||
*/
|
||||
SingleLogoutService getSingleLogoutService();
|
||||
PublicKey getSignatureValidationKey();
|
||||
|
||||
/**
|
||||
* Returns {@link KeyLocator} looking up public keys used for validation of IdP signatures.
|
||||
* @return see description.
|
||||
*/
|
||||
KeyLocator getSignatureValidationKeyLocator();
|
||||
|
||||
/**
|
||||
* Returns minimum time (in seconds) between issuing requests to IdP SAML descriptor.
|
||||
* Used e.g. by {@link KeyLocator} looking up public keys for validation of IdP signatures
|
||||
* to prevent too frequent requests.
|
||||
*
|
||||
* @return see description.
|
||||
*/
|
||||
int getMinTimeBetweenDescriptorRequests();
|
||||
|
||||
/**
|
||||
* Returns {@link HttpClient} instance that will be used for http communication with this IdP.
|
||||
* @return see description
|
||||
*/
|
||||
HttpClient getClient();
|
||||
|
||||
public interface SingleSignOnService {
|
||||
/**
|
||||
* Returns {@code true} if the requests to IdP need to be signed by SP key.
|
||||
* @return see dscription
|
||||
*/
|
||||
boolean signRequest();
|
||||
/**
|
||||
* Returns {@code true} if the complete response message from IdP should
|
||||
* be checked for valid signature.
|
||||
* @return see dscription
|
||||
*/
|
||||
boolean validateResponseSignature();
|
||||
/**
|
||||
* Returns {@code true} if individual assertions in response from IdP should
|
||||
* be checked for valid signature.
|
||||
* @return see dscription
|
||||
*/
|
||||
boolean validateAssertionSignature();
|
||||
Binding getRequestBinding();
|
||||
Binding getResponseBinding();
|
||||
String getRequestBindingUrl();
|
||||
}
|
||||
|
||||
public interface SingleLogoutService {
|
||||
boolean validateRequestSignature();
|
||||
boolean validateResponseSignature();
|
||||
@@ -67,10 +119,19 @@ public interface SamlDeployment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Identity Provider configuration for this SAML deployment.
|
||||
* @return see description.
|
||||
*/
|
||||
public IDP getIDP();
|
||||
|
||||
public boolean isConfigured();
|
||||
SslRequired getSslRequired();
|
||||
|
||||
/**
|
||||
* Returns entity identifier of this SP.
|
||||
* @return see description.
|
||||
*/
|
||||
String getEntityID();
|
||||
String getNameIDPolicyFormat();
|
||||
boolean isForceAuthentication();
|
||||
|
||||
@@ -40,7 +40,7 @@ public class SamlUtil {
|
||||
httpFacade.getResponse().setHeader("Content-Type", "text/html");
|
||||
httpFacade.getResponse().setHeader("Pragma", "no-cache");
|
||||
httpFacade.getResponse().setHeader("Cache-Control", "no-cache, no-store");
|
||||
httpFacade.getResponse().getOutputStream().write(html.getBytes());
|
||||
httpFacade.getResponse().getOutputStream().write(html.getBytes(GeneralConstants.SAML_CHARSET));
|
||||
httpFacade.getResponse().end();
|
||||
} else {
|
||||
String uri = asRequest ? binding.redirectBinding(document).requestURI(actionUrl).toString() : binding.redirectBinding(document).responseURI(actionUrl).toString();
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.keycloak.adapters.saml.config;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import org.keycloak.adapters.cloned.AdapterHttpClientConfig;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
@@ -157,12 +158,97 @@ public class IDP implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
public static class HttpClientConfig implements AdapterHttpClientConfig {
|
||||
|
||||
private String truststore;
|
||||
private String truststorePassword;
|
||||
private String clientKeystore;
|
||||
private String clientKeystorePassword;
|
||||
private boolean allowAnyHostname;
|
||||
private boolean disableTrustManager;
|
||||
private int connectionPoolSize;
|
||||
private String proxyUrl;
|
||||
|
||||
@Override
|
||||
public String getTruststore() {
|
||||
return truststore;
|
||||
}
|
||||
|
||||
public void setTruststore(String truststore) {
|
||||
this.truststore = truststore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTruststorePassword() {
|
||||
return truststorePassword;
|
||||
}
|
||||
|
||||
public void setTruststorePassword(String truststorePassword) {
|
||||
this.truststorePassword = truststorePassword;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientKeystore() {
|
||||
return clientKeystore;
|
||||
}
|
||||
|
||||
public void setClientKeystore(String clientKeystore) {
|
||||
this.clientKeystore = clientKeystore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientKeystorePassword() {
|
||||
return clientKeystorePassword;
|
||||
}
|
||||
|
||||
public void setClientKeystorePassword(String clientKeystorePassword) {
|
||||
this.clientKeystorePassword = clientKeystorePassword;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAllowAnyHostname() {
|
||||
return allowAnyHostname;
|
||||
}
|
||||
|
||||
public void setAllowAnyHostname(boolean allowAnyHostname) {
|
||||
this.allowAnyHostname = allowAnyHostname;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDisableTrustManager() {
|
||||
return disableTrustManager;
|
||||
}
|
||||
|
||||
public void setDisableTrustManager(boolean disableTrustManager) {
|
||||
this.disableTrustManager = disableTrustManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getConnectionPoolSize() {
|
||||
return connectionPoolSize;
|
||||
}
|
||||
|
||||
public void setConnectionPoolSize(int connectionPoolSize) {
|
||||
this.connectionPoolSize = connectionPoolSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProxyUrl() {
|
||||
return proxyUrl;
|
||||
}
|
||||
|
||||
public void setProxyUrl(String proxyUrl) {
|
||||
this.proxyUrl = proxyUrl;
|
||||
}
|
||||
}
|
||||
|
||||
private String entityID;
|
||||
private String signatureAlgorithm;
|
||||
private String signatureCanonicalizationMethod;
|
||||
private SingleSignOnService singleSignOnService;
|
||||
private SingleLogoutService singleLogoutService;
|
||||
private List<Key> keys;
|
||||
private AdapterHttpClientConfig httpClientConfig = new HttpClientConfig();
|
||||
|
||||
public String getEntityID() {
|
||||
return entityID;
|
||||
@@ -212,4 +298,12 @@ public class IDP implements Serializable {
|
||||
this.signatureCanonicalizationMethod = signatureCanonicalizationMethod;
|
||||
}
|
||||
|
||||
public AdapterHttpClientConfig getHttpClientConfig() {
|
||||
return httpClientConfig;
|
||||
}
|
||||
|
||||
public void setHttpClientConfig(AdapterHttpClientConfig httpClientConfig) {
|
||||
this.httpClientConfig = httpClientConfig;
|
||||
}
|
||||
|
||||
}
|
||||
+11
@@ -72,4 +72,15 @@ public class ConfigXmlConstants {
|
||||
public static final String VALIDATE_REQUEST_SIGNATURE_ATTR = "validateRequestSignature";
|
||||
public static final String POST_BINDING_URL_ATTR = "postBindingUrl";
|
||||
public static final String REDIRECT_BINDING_URL_ATTR = "redirectBindingUrl";
|
||||
|
||||
public static final String HTTP_CLIENT_ELEMENT = "HttpClient";
|
||||
public static final String ALLOW_ANY_HOSTNAME_ATTR = "allowAnyHostname";
|
||||
public static final String CLIENT_KEYSTORE_ATTR = "clientKeystore";
|
||||
public static final String CLIENT_KEYSTORE_PASSWORD_ATTR = "clientKeystorePassword";
|
||||
public static final String CONNECTION_POOL_SIZE_ATTR = "connectionPoolSize";
|
||||
public static final String DISABLE_TRUST_MANAGER_ATTR = "disableTrustManager";
|
||||
public static final String PROXY_URL_ATTR = "proxyUrl";
|
||||
public static final String TRUSTSTORE_ATTR = "truststore";
|
||||
public static final String TRUSTSTORE_PASSWORD_ATTR = "truststorePassword";
|
||||
|
||||
}
|
||||
+26
-21
@@ -40,6 +40,7 @@ import java.security.PublicKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.keycloak.adapters.cloned.HttpClientBuilder;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
@@ -178,35 +179,39 @@ public class DeploymentBuilder {
|
||||
if (sp.getIdp().getKeys() != null) {
|
||||
for (Key key : sp.getIdp().getKeys()) {
|
||||
if (key.isSigning()) {
|
||||
if (key.getKeystore() != null) {
|
||||
KeyStore keyStore = loadKeystore(resourceLoader, key);
|
||||
Certificate cert = null;
|
||||
try {
|
||||
cert = keyStore.getCertificate(key.getKeystore().getCertificateAlias());
|
||||
} catch (KeyStoreException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
idp.setSignatureValidationKey(cert.getPublicKey());
|
||||
} else {
|
||||
if (key.getPublicKeyPem() == null && key.getCertificatePem() == null) {
|
||||
throw new RuntimeException("IDP signing key must have a PublicKey or Certificate defined");
|
||||
}
|
||||
try {
|
||||
PublicKey publicKey = getPublicKeyFromPem(key);
|
||||
idp.setSignatureValidationKey(publicKey);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
processSigningKey(idp, key, resourceLoader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
idp.setClient(new HttpClientBuilder().build(sp.getIdp().getHttpClientConfig()));
|
||||
idp.refreshKeyLocatorConfiguration();
|
||||
|
||||
return deployment;
|
||||
}
|
||||
|
||||
protected static PublicKey getPublicKeyFromPem(Key key) throws Exception {
|
||||
private void processSigningKey(DefaultSamlDeployment.DefaultIDP idp, Key key, ResourceLoader resourceLoader) throws RuntimeException {
|
||||
PublicKey publicKey;
|
||||
if (key.getKeystore() != null) {
|
||||
KeyStore keyStore = loadKeystore(resourceLoader, key);
|
||||
Certificate cert = null;
|
||||
try {
|
||||
cert = keyStore.getCertificate(key.getKeystore().getCertificateAlias());
|
||||
} catch (KeyStoreException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
publicKey = cert.getPublicKey();
|
||||
} else {
|
||||
if (key.getPublicKeyPem() == null && key.getCertificatePem() == null) {
|
||||
throw new RuntimeException("IDP signing key must have a PublicKey or Certificate defined");
|
||||
}
|
||||
publicKey = getPublicKeyFromPem(key);
|
||||
}
|
||||
|
||||
idp.addSignatureValidationKey(publicKey);
|
||||
}
|
||||
|
||||
protected static PublicKey getPublicKeyFromPem(Key key) {
|
||||
PublicKey publicKey;
|
||||
if (key.getPublicKeyPem() != null) {
|
||||
publicKey = PemUtils.decodePublicKey(key.getPublicKeyPem().trim());
|
||||
|
||||
+60
-18
@@ -29,6 +29,10 @@ import javax.xml.stream.events.EndElement;
|
||||
import javax.xml.stream.events.StartElement;
|
||||
import javax.xml.stream.events.XMLEvent;
|
||||
import java.util.List;
|
||||
import org.keycloak.adapters.saml.config.IDP.HttpClientConfig;
|
||||
import static org.keycloak.adapters.saml.config.parsers.SPXmlParser.getAttributeValue;
|
||||
import static org.keycloak.adapters.saml.config.parsers.SPXmlParser.getBooleanAttributeValue;
|
||||
import static org.keycloak.adapters.saml.config.parsers.SPXmlParser.getIntegerAttributeValue;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
@@ -41,16 +45,16 @@ public class IDPXmlParser extends AbstractParser {
|
||||
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
|
||||
StaxParserUtil.validate(startElement, ConfigXmlConstants.IDP_ELEMENT);
|
||||
IDP idp = new IDP();
|
||||
String entityID = SPXmlParser.getAttributeValue(startElement, ConfigXmlConstants.ENTITY_ID_ATTR);
|
||||
String entityID = getAttributeValue(startElement, ConfigXmlConstants.ENTITY_ID_ATTR);
|
||||
if (entityID == null) {
|
||||
throw new ParsingException("entityID must be set on IDP");
|
||||
|
||||
}
|
||||
idp.setEntityID(entityID);
|
||||
|
||||
boolean signaturesRequired = SPXmlParser.getBooleanAttributeValue(startElement, ConfigXmlConstants.SIGNATURES_REQUIRED_ATTR);
|
||||
idp.setSignatureCanonicalizationMethod(SPXmlParser.getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_CANONICALIZATION_METHOD_ATTR));
|
||||
idp.setSignatureAlgorithm(SPXmlParser.getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_ALGORITHM_ATTR));
|
||||
boolean signaturesRequired = getBooleanAttributeValue(startElement, ConfigXmlConstants.SIGNATURES_REQUIRED_ATTR);
|
||||
idp.setSignatureCanonicalizationMethod(getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_CANONICALIZATION_METHOD_ATTR));
|
||||
idp.setSignatureAlgorithm(getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_ALGORITHM_ATTR));
|
||||
while (xmlEventReader.hasNext()) {
|
||||
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
|
||||
if (xmlEvent == null)
|
||||
@@ -75,6 +79,10 @@ public class IDPXmlParser extends AbstractParser {
|
||||
IDP.SingleLogoutService slo = parseSingleLogoutService(xmlEventReader, signaturesRequired);
|
||||
idp.setSingleLogoutService(slo);
|
||||
|
||||
} else if (tag.equals(ConfigXmlConstants.HTTP_CLIENT_ELEMENT)) {
|
||||
HttpClientConfig config = parseHttpClientElement(xmlEventReader);
|
||||
idp.setHttpClientConfig(config);
|
||||
|
||||
} else if (tag.equals(ConfigXmlConstants.KEYS_ELEMENT)) {
|
||||
KeysXmlParser parser = new KeysXmlParser();
|
||||
List<Key> keys = (List<Key>)parser.parse(xmlEventReader);
|
||||
@@ -90,29 +98,63 @@ public class IDPXmlParser extends AbstractParser {
|
||||
protected IDP.SingleLogoutService parseSingleLogoutService(XMLEventReader xmlEventReader, boolean signaturesRequired) throws ParsingException {
|
||||
IDP.SingleLogoutService slo = new IDP.SingleLogoutService();
|
||||
StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader);
|
||||
slo.setSignRequest(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
|
||||
slo.setValidateResponseSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
|
||||
slo.setValidateRequestSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_REQUEST_SIGNATURE_ATTR, signaturesRequired));
|
||||
slo.setRequestBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
|
||||
slo.setResponseBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
|
||||
slo.setSignResponse(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_RESPONSE_ATTR, signaturesRequired));
|
||||
slo.setPostBindingUrl(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.POST_BINDING_URL_ATTR));
|
||||
slo.setRedirectBindingUrl(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.REDIRECT_BINDING_URL_ATTR));
|
||||
slo.setSignRequest(getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
|
||||
slo.setValidateResponseSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
|
||||
slo.setValidateRequestSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_REQUEST_SIGNATURE_ATTR, signaturesRequired));
|
||||
slo.setRequestBinding(getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
|
||||
slo.setResponseBinding(getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
|
||||
slo.setSignResponse(getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_RESPONSE_ATTR, signaturesRequired));
|
||||
slo.setPostBindingUrl(getAttributeValue(element, ConfigXmlConstants.POST_BINDING_URL_ATTR));
|
||||
slo.setRedirectBindingUrl(getAttributeValue(element, ConfigXmlConstants.REDIRECT_BINDING_URL_ATTR));
|
||||
return slo;
|
||||
}
|
||||
|
||||
protected IDP.SingleSignOnService parseSingleSignOnService(XMLEventReader xmlEventReader, boolean signaturesRequired) throws ParsingException {
|
||||
IDP.SingleSignOnService sso = new IDP.SingleSignOnService();
|
||||
StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader);
|
||||
sso.setSignRequest(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
|
||||
sso.setValidateResponseSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
|
||||
sso.setValidateAssertionSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_ASSERTION_SIGNATURE_ATTR));
|
||||
sso.setRequestBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
|
||||
sso.setResponseBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
|
||||
sso.setBindingUrl(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.BINDING_URL_ATTR));
|
||||
sso.setSignRequest(getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
|
||||
sso.setValidateResponseSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
|
||||
sso.setValidateAssertionSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_ASSERTION_SIGNATURE_ATTR));
|
||||
sso.setRequestBinding(getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
|
||||
sso.setResponseBinding(getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
|
||||
sso.setBindingUrl(getAttributeValue(element, ConfigXmlConstants.BINDING_URL_ATTR));
|
||||
return sso;
|
||||
}
|
||||
|
||||
private HttpClientConfig parseHttpClientElement(XMLEventReader xmlEventReader) throws ParsingException {
|
||||
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
|
||||
StaxParserUtil.validate(startElement, ConfigXmlConstants.HTTP_CLIENT_ELEMENT);
|
||||
HttpClientConfig config = new HttpClientConfig();
|
||||
|
||||
config.setAllowAnyHostname(getBooleanAttributeValue(startElement, ConfigXmlConstants.ALLOW_ANY_HOSTNAME_ATTR, false));
|
||||
config.setClientKeystore(getAttributeValue(startElement, ConfigXmlConstants.CLIENT_KEYSTORE_ATTR));
|
||||
config.setClientKeystorePassword(getAttributeValue(startElement, ConfigXmlConstants.CLIENT_KEYSTORE_PASSWORD_ATTR));
|
||||
config.setConnectionPoolSize(getIntegerAttributeValue(startElement, ConfigXmlConstants.CONNECTION_POOL_SIZE_ATTR, 0));
|
||||
config.setDisableTrustManager(getBooleanAttributeValue(startElement, ConfigXmlConstants.ALLOW_ANY_HOSTNAME_ATTR, false));
|
||||
config.setProxyUrl(getAttributeValue(startElement, ConfigXmlConstants.PROXY_URL_ATTR));
|
||||
config.setTruststore(getAttributeValue(startElement, ConfigXmlConstants.TRUSTSTORE_ATTR));
|
||||
config.setTruststorePassword(getAttributeValue(startElement, ConfigXmlConstants.TRUSTSTORE_PASSWORD_ATTR));
|
||||
|
||||
while (xmlEventReader.hasNext()) {
|
||||
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
|
||||
if (xmlEvent == null)
|
||||
break;
|
||||
if (xmlEvent instanceof EndElement) {
|
||||
EndElement endElement = (EndElement) StaxParserUtil.getNextEvent(xmlEventReader);
|
||||
String endElementName = StaxParserUtil.getEndElementName(endElement);
|
||||
if (endElementName.equals(ConfigXmlConstants.ROLE_IDENTIFIERS_ELEMENT))
|
||||
break;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
|
||||
String tag = StaxParserUtil.getStartElementName(startElement);
|
||||
StaxParserUtil.bypassElementBlock(xmlEventReader, tag);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(QName qname) {
|
||||
return false;
|
||||
|
||||
+7
@@ -48,6 +48,13 @@ public class SPXmlParser extends AbstractParser {
|
||||
return str;
|
||||
}
|
||||
|
||||
public static int getIntegerAttributeValue(StartElement startElement, String tag, int defaultValue) {
|
||||
String result = getAttributeValue(startElement, tag);
|
||||
if (result == null)
|
||||
return defaultValue;
|
||||
return Integer.valueOf(result);
|
||||
}
|
||||
|
||||
public static boolean getBooleanAttributeValue(StartElement startElement, String tag, boolean defaultValue) {
|
||||
String result = getAttributeValue(startElement, tag);
|
||||
if (result == null)
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.saml.descriptor.parsers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import javax.xml.crypto.MarshalException;
|
||||
import javax.xml.crypto.dom.DOMStructure;
|
||||
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
|
||||
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.xpath.XPath;
|
||||
import javax.xml.xpath.XPathConstants;
|
||||
import javax.xml.xpath.XPathExpression;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLConstants;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.processing.core.util.NamespaceContext;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
/**
|
||||
* Goes through the given XML file and extracts names, certificates and keys from the KeyInfo elements.
|
||||
* @author hmlnarik
|
||||
*/
|
||||
public class SamlDescriptorIDPKeysExtractor {
|
||||
|
||||
private static final NamespaceContext NS_CONTEXT = new NamespaceContext();
|
||||
static {
|
||||
NS_CONTEXT.addNsUriPair("m", JBossSAMLURIConstants.METADATA_NSURI.get());
|
||||
NS_CONTEXT.addNsUriPair("dsig", JBossSAMLURIConstants.XMLDSIG_NSURI.get());
|
||||
}
|
||||
|
||||
private final KeyInfoFactory kif = KeyInfoFactory.getInstance();
|
||||
|
||||
private final XPathFactory xPathfactory = XPathFactory.newInstance();
|
||||
private final XPath xpath = xPathfactory.newXPath();
|
||||
{
|
||||
xpath.setNamespaceContext(NS_CONTEXT);
|
||||
}
|
||||
|
||||
public MultivaluedHashMap<String, KeyInfo> parse(InputStream stream) throws ParsingException {
|
||||
MultivaluedHashMap<String, KeyInfo> res = new MultivaluedHashMap<>();
|
||||
|
||||
try {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setNamespaceAware(true);
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
Document doc = builder.parse(stream);
|
||||
|
||||
XPathExpression expr = xpath.compile("/m:EntitiesDescriptor/m:EntityDescriptor/m:IDPSSODescriptor/m:KeyDescriptor");
|
||||
NodeList keyDescriptors = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
|
||||
for (int i = 0; i < keyDescriptors.getLength(); i ++) {
|
||||
Node keyDescriptor = keyDescriptors.item(i);
|
||||
Element keyDescriptorEl = (Element) keyDescriptor;
|
||||
KeyInfo ki = processKeyDescriptor(keyDescriptorEl);
|
||||
if (ki != null) {
|
||||
String use = keyDescriptorEl.getAttribute(JBossSAMLConstants.USE.get());
|
||||
res.add(use, ki);
|
||||
}
|
||||
}
|
||||
} catch (SAXException | IOException | ParserConfigurationException | MarshalException | XPathExpressionException e) {
|
||||
throw new ParsingException("Error parsing SAML descriptor", e);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private KeyInfo processKeyDescriptor(Element keyDescriptor) throws MarshalException {
|
||||
NodeList childNodes = keyDescriptor.getElementsByTagNameNS(JBossSAMLURIConstants.XMLDSIG_NSURI.get(), JBossSAMLConstants.KEY_INFO.get());
|
||||
|
||||
if (childNodes.getLength() == 0) {
|
||||
return null;
|
||||
}
|
||||
Node keyInfoNode = childNodes.item(0);
|
||||
return (keyInfoNode == null) ? null : kif.unmarshalKeyInfo(new DOMStructure(keyInfoNode));
|
||||
}
|
||||
|
||||
}
|
||||
+109
-9
@@ -64,11 +64,20 @@ import org.w3c.dom.Node;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||
import org.keycloak.rotation.KeyLocator;
|
||||
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -257,13 +266,44 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
|
||||
}
|
||||
|
||||
private void validateSamlSignature(SAMLDocumentHolder holder, boolean postBinding, String paramKey) throws VerificationException {
|
||||
KeyLocator signatureValidationKey = deployment.getIDP().getSignatureValidationKeyLocator();
|
||||
if (postBinding) {
|
||||
verifyPostBindingSignature(holder.getSamlDocument(), deployment.getIDP().getSignatureValidationKey());
|
||||
verifyPostBindingSignature(holder.getSamlDocument(), signatureValidationKey);
|
||||
} else {
|
||||
verifyRedirectBindingSignature(deployment.getIDP().getSignatureValidationKey(), paramKey);
|
||||
String keyId = getMessageSigningKeyId(holder.getSamlObject());
|
||||
verifyRedirectBindingSignature(paramKey, signatureValidationKey, keyId);
|
||||
}
|
||||
}
|
||||
|
||||
private String getMessageSigningKeyId(SAML2Object doc) {
|
||||
final ExtensionsType extensions;
|
||||
if (doc instanceof RequestAbstractType) {
|
||||
extensions = ((RequestAbstractType) doc).getExtensions();
|
||||
} else if (doc instanceof StatusResponseType) {
|
||||
extensions = ((StatusResponseType) doc).getExtensions();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (extensions == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Object ext : extensions.getAny()) {
|
||||
if (! (ext instanceof Element)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String res = KeycloakKeySamlExtensionGenerator.getMessageSigningKeyIdFromElement((Element) ext);
|
||||
|
||||
if (res != null) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){
|
||||
if(statusCode != null && statusCode.getValue()!=null){
|
||||
String v = statusCode.getValue().toString();
|
||||
@@ -473,10 +513,10 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
|
||||
return false;
|
||||
}
|
||||
|
||||
public void verifyPostBindingSignature(Document document, PublicKey publicKey) throws VerificationException {
|
||||
public void verifyPostBindingSignature(Document document, KeyLocator keyLocator) throws VerificationException {
|
||||
SAML2Signature saml2Signature = new SAML2Signature();
|
||||
try {
|
||||
if (!saml2Signature.validate(document, publicKey)) {
|
||||
if (!saml2Signature.validate(document, keyLocator)) {
|
||||
throw new VerificationException("Invalid signature on document");
|
||||
}
|
||||
} catch (ProcessingException e) {
|
||||
@@ -484,7 +524,7 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
|
||||
}
|
||||
}
|
||||
|
||||
public void verifyRedirectBindingSignature(PublicKey publicKey, String paramKey) throws VerificationException {
|
||||
private void verifyRedirectBindingSignature(String paramKey, KeyLocator keyLocator, String keyId) throws VerificationException {
|
||||
String request = facade.getRequest().getQueryParamValue(paramKey);
|
||||
String algorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
|
||||
String signature = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY);
|
||||
@@ -511,16 +551,76 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
|
||||
try {
|
||||
//byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature);
|
||||
byte[] decodedSignature = Base64.decode(signature);
|
||||
byte[] rawQueryBytes = rawQuery.getBytes("UTF-8");
|
||||
|
||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm);
|
||||
Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg
|
||||
validator.initVerify(publicKey);
|
||||
validator.update(rawQuery.getBytes("UTF-8"));
|
||||
if (!validator.verify(decodedSignature)) {
|
||||
|
||||
if (! validateRedirectBindingSignature(signatureAlgorithm, rawQueryBytes, decodedSignature, keyLocator, keyId)) {
|
||||
throw new VerificationException("Invalid query param signature");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new VerificationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean validateRedirectBindingSignature(SignatureAlgorithm sigAlg, byte[] rawQueryBytes, byte[] decodedSignature, KeyLocator locator, String keyId)
|
||||
throws KeyManagementException, VerificationException {
|
||||
try {
|
||||
Key key;
|
||||
try {
|
||||
key = locator.getKey(keyId);
|
||||
boolean keyLocated = key != null;
|
||||
|
||||
if (keyLocated) {
|
||||
return validateRedirectBindingSignatureForKey(sigAlg, rawQueryBytes, decodedSignature, key);
|
||||
}
|
||||
} catch (KeyManagementException ex) {
|
||||
}
|
||||
} catch (SignatureException ex) {
|
||||
log.debug("Verification failed for key %s: %s", keyId, ex);
|
||||
log.trace(ex);
|
||||
}
|
||||
|
||||
if (locator instanceof Iterable) {
|
||||
Iterable<Key> availableKeys = (Iterable<Key>) locator;
|
||||
|
||||
log.trace("Trying hard to validate XML signature using all available keys.");
|
||||
|
||||
for (Key key : availableKeys) {
|
||||
try {
|
||||
if (validateRedirectBindingSignatureForKey(sigAlg, rawQueryBytes, decodedSignature, key)) {
|
||||
return true;
|
||||
}
|
||||
} catch (SignatureException ex) {
|
||||
log.debug("Verification failed: %s", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean validateRedirectBindingSignatureForKey(SignatureAlgorithm sigAlg, byte[] rawQueryBytes, byte[] decodedSignature, Key key)
|
||||
throws SignatureException {
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! (key instanceof PublicKey)) {
|
||||
log.warnf("Unusable key for signature validation: %s", key);
|
||||
return false;
|
||||
}
|
||||
|
||||
Signature signature = sigAlg.createSignature(); // todo plugin signature alg
|
||||
try {
|
||||
signature.initVerify((PublicKey) key);
|
||||
} catch (InvalidKeyException ex) {
|
||||
log.warnf(ex, "Unusable key for signature validation: %s", key);
|
||||
return false;
|
||||
}
|
||||
|
||||
signature.update(rawQueryBytes);
|
||||
|
||||
return signature.verify(decodedSignature);
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -82,8 +82,10 @@ public class WebBrowserSsoAuthenticationHandler extends AbstractSamlAuthenticati
|
||||
if (deployment.getSignatureCanonicalizationMethod() != null)
|
||||
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
|
||||
binding.signatureAlgorithm(deployment.getSignatureAlgorithm())
|
||||
.signWith(deployment.getSigningKeyPair())
|
||||
.signWith(null, deployment.getSigningKeyPair())
|
||||
.signDocument();
|
||||
// TODO: As part of KEYCLOAK-3810, add KeyID to the SAML document
|
||||
// <related DocumentBuilder>.addExtension(new KeycloakKeySamlExtensionGenerator(<key ID>));
|
||||
}
|
||||
|
||||
|
||||
@@ -113,8 +115,10 @@ public class WebBrowserSsoAuthenticationHandler extends AbstractSamlAuthenticati
|
||||
if (deployment.getSignatureCanonicalizationMethod() != null)
|
||||
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
|
||||
binding.signatureAlgorithm(deployment.getSignatureAlgorithm());
|
||||
binding.signWith(deployment.getSigningKeyPair())
|
||||
binding.signWith(null, deployment.getSigningKeyPair())
|
||||
.signDocument();
|
||||
// TODO: As part of KEYCLOAK-3810, add KeyID to the SAML document
|
||||
// <related DocumentBuilder>.addExtension(new KeycloakKeySamlExtensionGenerator(<key ID>));
|
||||
}
|
||||
|
||||
binding.relayState("logout");
|
||||
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* 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.saml.rotation;
|
||||
|
||||
import java.security.Key;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
|
||||
import javax.xml.crypto.dsig.keyinfo.KeyName;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.adapters.cloned.HttpAdapterUtils;
|
||||
import org.keycloak.adapters.cloned.HttpClientAdapterException;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
|
||||
import org.keycloak.rotation.KeyLocator;
|
||||
import org.keycloak.saml.processing.api.util.KeyInfoTools;
|
||||
|
||||
/**
|
||||
* This class defines a {@link KeyLocator} that looks up public keys and certificates in IdP's
|
||||
* SAML descriptor (i.e. http://{host}/auth/realms/{realm}/protocol/saml/descriptor).
|
||||
*
|
||||
* Based on {@code JWKPublicKeyLocator}.
|
||||
*
|
||||
* @author hmlnarik
|
||||
*/
|
||||
public class SamlDescriptorPublicKeyLocator implements KeyLocator, Iterable<PublicKey> {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(SamlDescriptorPublicKeyLocator.class);
|
||||
|
||||
/**
|
||||
* Time between two subsequent requests (in seconds).
|
||||
*/
|
||||
private final int minTimeBetweenDescriptorRequests;
|
||||
|
||||
/**
|
||||
* Time to live for cache entries (in seconds).
|
||||
*/
|
||||
private final int cacheEntryTtl;
|
||||
|
||||
/**
|
||||
* Target descriptor URL.
|
||||
*/
|
||||
private final String descriptorUrl;
|
||||
|
||||
private final Map<String, PublicKey> publicKeyCache = new ConcurrentHashMap<>();
|
||||
|
||||
private final HttpClient client;
|
||||
|
||||
private volatile int lastRequestTime = 0;
|
||||
|
||||
public SamlDescriptorPublicKeyLocator(String descriptorUrl, int minTimeBetweenDescriptorRequests, int cacheEntryTtl, HttpClient httpClient) {
|
||||
this.minTimeBetweenDescriptorRequests = minTimeBetweenDescriptorRequests <= 0
|
||||
? 20
|
||||
: minTimeBetweenDescriptorRequests;
|
||||
|
||||
this.descriptorUrl = descriptorUrl;
|
||||
this.cacheEntryTtl = cacheEntryTtl;
|
||||
|
||||
this.client = httpClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key getKey(String kid) throws KeyManagementException {
|
||||
if (kid == null) {
|
||||
LOG.debugf("Invalid key id: %s", kid);
|
||||
return null;
|
||||
}
|
||||
|
||||
LOG.tracef("Requested key id: %s", kid);
|
||||
|
||||
int currentTime = Time.currentTime();
|
||||
|
||||
PublicKey res;
|
||||
if (currentTime > this.lastRequestTime + this.cacheEntryTtl) {
|
||||
LOG.debugf("Performing regular cache cleanup.");
|
||||
res = refreshCertificateCacheAndGet(kid);
|
||||
} else {
|
||||
res = publicKeyCache.get(kid);
|
||||
|
||||
if (res == null) {
|
||||
if (currentTime > this.lastRequestTime + this.minTimeBetweenDescriptorRequests) {
|
||||
res = refreshCertificateCacheAndGet(kid);
|
||||
} else {
|
||||
LOG.debugf("Won't send request to realm SAML descriptor url, timeout not expired. Last request time was %d", lastRequestTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void refreshKeyCache() {
|
||||
LOG.info("Forcing key cache cleanup and refresh.");
|
||||
this.publicKeyCache.clear();
|
||||
refreshCertificateCacheAndGet(null);
|
||||
}
|
||||
|
||||
private synchronized PublicKey refreshCertificateCacheAndGet(String kid) {
|
||||
if (this.descriptorUrl == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.lastRequestTime = Time.currentTime();
|
||||
|
||||
LOG.debugf("Refreshing public key cache from %s", this.descriptorUrl);
|
||||
List<KeyInfo> signingCerts;
|
||||
try {
|
||||
MultivaluedHashMap<String, KeyInfo> certs = HttpAdapterUtils.downloadKeysFromSamlDescriptor(client, this.descriptorUrl);
|
||||
signingCerts = certs.get(KeyTypes.SIGNING.value());
|
||||
} catch (HttpClientAdapterException ex) {
|
||||
LOG.error("Could not refresh certificates from the server", ex);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (signingCerts == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LOG.debugf("Certificates retrieved from server, filling public key cache");
|
||||
|
||||
// Only clear cache after it is certain that the SAML descriptor has been read successfully
|
||||
this.publicKeyCache.clear();
|
||||
|
||||
for (KeyInfo ki : signingCerts) {
|
||||
KeyName keyName = KeyInfoTools.getKeyName(ki);
|
||||
X509Certificate x509certificate = KeyInfoTools.getX509Certificate(ki);
|
||||
if (x509certificate != null && keyName != null) {
|
||||
LOG.tracef("Registering signing certificate %s", keyName.getName());
|
||||
this.publicKeyCache.put(keyName.getName(), x509certificate.getPublicKey());
|
||||
} else {
|
||||
LOG.tracef("Ignoring certificate %s: %s", keyName, x509certificate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return (kid == null ? null : this.publicKeyCache.get(kid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Keys retrieved from SAML descriptor at " + descriptorUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<PublicKey> iterator() {
|
||||
if (this.publicKeyCache.isEmpty()) {
|
||||
refreshCertificateCacheAndGet(null);
|
||||
}
|
||||
|
||||
return this.publicKeyCache.values().iterator();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<xs:schema version="1.0"
|
||||
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="urn:keycloak:saml:adapter"
|
||||
targetNamespace="urn:keycloak:saml:adapter"
|
||||
elementFormDefault="qualified"
|
||||
attributeFormDefault="unqualified">
|
||||
|
||||
<xs:element name="keycloak-saml-adapter" type="adapter-type"/>
|
||||
<xs:complexType name="adapter-type">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Keycloak SAML Adapter configuration file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:all>
|
||||
<xs:element name="SP" maxOccurs="1" minOccurs="0" type="sp-type">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Describes SAML service provider configuration.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="sp-type">
|
||||
<xs:all>
|
||||
<xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
List of service provider encryption and validation keys.
|
||||
|
||||
If the IDP requires that the client application (SP) sign all of its requests and/or if the IDP will encrypt assertions, you must define the keys used to do this. For client signed documents you must define both the private and public key or certificate that will be used to sign documents. For encryption, you only have to define the private key that will be used to decrypt.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="PrincipalNameMapping" type="principal-name-mapping-type" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>When creating a Java Principal object that you obtain from methods like HttpServletRequest.getUserPrincipal(), you can define what name that is returned by the Principal.getName() method.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="RoleIdentifiers" type="role-identifiers-type" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Defines what SAML attributes within the assertion received from the user should be used as role identifiers within the Java EE Security Context for the user.
|
||||
By default Role attribute values are converted to Java EE roles. Some IDPs send roles via a member or memberOf attribute assertion. You can define one or more Attribute elements to specify which SAML attributes must be converted into roles.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="IDP" type="idp-type" minOccurs="1" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Describes configuration of SAML identity provider for this service provider.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="entityID" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This is the identifier for this client. The IDP needs this value to determine who the client is that is communicating with it.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="sslPolicy" type="ssl-policy-type" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>SSL policy the adapter will enforce.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="nameIDPolicyFormat" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>SAML clients can request a specific NameID Subject format. Fill in this value if you want a specific format. It must be a standard SAML format identifier, i.e. urn:oasis:names:tc:SAML:2.0:nameid-format:transient. By default, no special format is requested.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="logoutPage" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>URL of the logout page.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="forceAuthentication" type="xs:boolean" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>SAML clients can request that a user is re-authenticated even if they are already logged in at the IDP. Default value is false.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="isPassive" type="xs:boolean" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>SAML clients can request that a user is never asked to authenticate even if they are not logged in at the IDP. Set this to true if you want this. Do not use together with forceAuthentication as they are opposite. Default value is false.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="turnOffChangeSessionIdOnLogin" type="xs:boolean" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The session id is changed by default on a successful login on some platforms to plug a security attack vector. Change this to true to disable this. It is recommended you do not turn it off. Default value is false.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="keys-type">
|
||||
<xs:sequence>
|
||||
<xs:element name="Key" type="key-type" minOccurs="1" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Describes a single key used for signing or encryption.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="key-type">
|
||||
<xs:all>
|
||||
<xs:element name="KeyStore" maxOccurs="1" minOccurs="0" type="key-store-type">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Java keystore to load keys and certificates from.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="PrivateKeyPem" type="xs:string" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Private key (PEM format)</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="PublicKeyPem" type="xs:string" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Public key (PEM format)</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="CertificatePem" type="xs:string" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Certificate key (PEM format)</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="signing" type="xs:boolean" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Flag defining whether the key should be used for signing.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="encryption" type="xs:boolean" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Flag defining whether the key should be used for encryption</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="key-store-type">
|
||||
<xs:all>
|
||||
<xs:element name="PrivateKey" maxOccurs="1" minOccurs="0" type="private-key-type">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Private key declaration</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="Certificate" type="certificate-type" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Certificate declaration</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="file" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>File path to the key store.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="resource" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>WAR resource path to the key store. This is a path used in method call to ServletContext.getResourceAsStream().</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="password" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The password of the key store.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="private-key-type">
|
||||
<xs:attribute name="alias" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Alias that points to the key or cert within the keystore.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="password" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Keystores require an additional password to access private keys. In the PrivateKey element you must define this password within a password attribute.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="certificate-type">
|
||||
<xs:attribute name="alias" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Alias that points to the key or cert within the keystore.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="principal-name-mapping-type">
|
||||
<xs:attribute name="policy" type="principal-name-mapping-policy-type" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Policy used to populate value of Java Principal object obtained from methods like HttpServletRequest.getUserPrincipal().</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="attribute" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Name of the SAML assertion attribute to use within.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
<xs:simpleType name="principal-name-mapping-policy-type">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="FROM_NAME_ID">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This policy just uses whatever the SAML subject value is. This is the default setting</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="FROM_ATTRIBUTE">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This will pull the value from one of the attributes declared in the SAML assertion received from the server. You'll need to specify the name of the SAML assertion attribute to use within the attribute XML attribute.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
<xs:simpleType name="ssl-policy-type">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="ALL">
|
||||
<xs:annotation>
|
||||
<xs:documentation>All requests must come in via HTTPS.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="EXTERNAL">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Only non-private IP addresses must come over the wire via HTTPS.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
<xs:enumeration value="NONE">
|
||||
<xs:annotation>
|
||||
<xs:documentation>no requests are required to come over via HTTPS.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:enumeration>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
<xs:simpleType name="signature-algorithm-type">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="RSA_SHA1"/>
|
||||
<xs:enumeration value="RSA_SHA256"/>
|
||||
<xs:enumeration value="RSA_SHA512"/>
|
||||
<xs:enumeration value="DSA_SHA1"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
<xs:simpleType name="binding-type">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="POST"/>
|
||||
<xs:enumeration value="REDIRECT"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
<xs:complexType name="role-identifiers-type">
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="Attribute" maxOccurs="unbounded" minOccurs="0" type="attribute-type">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Specifies SAML attribute to be converted into roles.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:choice>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="attribute-type">
|
||||
<xs:attribute name="name" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Specifies name of the SAML attribute to be converted into roles.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="idp-type">
|
||||
<xs:sequence minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="SingleSignOnService" maxOccurs="1" minOccurs="1" type="sign-on-type">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Configuration of the login SAML endpoint of the IDP.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="SingleLogoutService" type="logout-type" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Configuration of the logout SAML endpoint of the IDP</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The Keys sub element of IDP is only used to define the certificate or public key to use to verify documents signed by the IDP.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="HttpClient" type="http-client-type" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Configuration of HTTP client used for automatic obtaining of certificates containing public keys for IDP signature verification via SAML descriptor of the IDP.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="entityID" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>issuer ID of the IDP.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="signaturesRequired" type="xs:boolean" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>If set to true, the client adapter will sign every document it sends to the IDP. Also, the client will expect that the IDP will be signing any documents sent to it. This switch sets the default for all request and response types.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="signatureAlgorithm" type="signature-algorithm-type" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Signature algorithm that the IDP expects signed documents to use. Defaults to RSA_SHA256</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="signatureCanonicalizationMethod" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This is the signature canonicalization method that the IDP expects signed documents to use. The default value is http://www.w3.org/2001/10/xml-exc-c14n# and should be good for most IDPs.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="encryption" type="xs:boolean" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation></xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="sign-on-type">
|
||||
<xs:attribute name="signRequest" type="xs:boolean" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Should the client sign authn requests? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Should the client expect the IDP to sign the assertion response document sent back from an auhtn request? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="validateAssertionSignature" type="xs:boolean" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Should the client expect the IDP to sign the individual assertions sent back from an auhtn request? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="requestBinding" type="binding-type" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>SAML binding type used for communicating with the IDP. The default value is POST, but you can set it to REDIRECT as well.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="responseBinding" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>SAML allows the client to request what binding type it wants authn responses to use. The default is that the client will not request a specific binding type for responses.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="bindingUrl" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This is the URL for the IDP login service that the client will send requests to.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="logout-type">
|
||||
<xs:attribute name="signRequest" type="xs:boolean" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Should the client sign authn requests? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="signResponse" type="xs:boolean" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Should the client sign logout responses it sends to the IDP requests? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="validateRequestSignature" type="xs:boolean" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Should the client expect signed logout request documents from the IDP? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Should the client expect signed logout response documents from the IDP? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="requestBinding" type="binding-type" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This is the SAML binding type used for communicating SAML requests to the IDP. The default value is POST.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="responseBinding" type="binding-type" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This is the SAML binding type used for communicating SAML responses to the IDP. The default value is POST.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="postBindingUrl" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This is the URL for the IDP's logout service when using the POST binding. This setting is REQUIRED if using the POST binding.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="redirectBindingUrl" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This is the URL for the IDP's logout service when using the REDIRECT binding. This setting is REQUIRED if using the REDIRECT binding.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="http-client-type">
|
||||
<xs:attribute name="allowAnyHostname" type="xs:boolean" use="optional" default="false">
|
||||
<xs:annotation>
|
||||
<xs:documentation>If the the IDP server requires HTTPS and this config option is set to true the IDP's certificate
|
||||
is validated via the truststore, but host name validation is not done. This setting should only be used during
|
||||
development and never in production as it will partly disable verification of SSL certificates.
|
||||
This seting may be useful in test environments. The default value is false.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="clientKeystore" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This is the file path to a keystore file. This keystore contains client certificate
|
||||
for two-way SSL when the adapter makes HTTPS requests to the IDP server.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="clientKeystorePassword" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Password for the client keystore and for the client's key.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="connectionPoolSize" type="xs:int" use="optional" default="10">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Defines number of pooled connections.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="disableTrustManager" type="xs:boolean" use="optional" default="false">
|
||||
<xs:annotation>
|
||||
<xs:documentation>If the the IDP server requires HTTPS and this config option is set to true you do not have to specify a truststore.
|
||||
This setting should only be used during development and never in production as it will disable verification of SSL certificates.
|
||||
The default value is false.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="proxyUrl" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>URL to HTTP proxy to use for HTTP connections.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="truststore" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The value is the file path to a keystore file. If you prefix the path with classpath:,
|
||||
then the truststore will be obtained from the deployment's classpath instead. Used for outgoing
|
||||
HTTPS communications to the IDP server. Client making HTTPS requests need
|
||||
a way to verify the host of the server they are talking to. This is what the trustore does.
|
||||
The keystore contains one or more trusted host certificates or certificate authorities.
|
||||
You can create this truststore by extracting the public certificate of the IDP's SSL keystore.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="truststorePassword" type="xs:string" use="optional">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Password for the truststore keystore.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
</xs:schema>
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package org.keycloak.adapters.cloned;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
|
||||
import javax.xml.crypto.dsig.keyinfo.KeyName;
|
||||
import javax.xml.crypto.dsig.keyinfo.X509Data;
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
import org.keycloak.adapters.saml.config.parsers.ConfigXmlConstants;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author hmlnarik
|
||||
*/
|
||||
public class HttpAdapterUtilsTest {
|
||||
|
||||
private <T> T getContent(List<Object> objects, Class<T> clazz) {
|
||||
for (Object o : objects) {
|
||||
if (clazz.isInstance(o)) {
|
||||
return (T) o;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractKeysFromSamlDescriptor() throws ParsingException {
|
||||
InputStream xmlStream = HttpAdapterUtilsTest.class.getResourceAsStream("saml-descriptor-valid.xml");
|
||||
MultivaluedHashMap<String, KeyInfo> res = HttpAdapterUtils.extractKeysFromSamlDescriptor(xmlStream);
|
||||
|
||||
assertThat(res, notNullValue());
|
||||
assertThat(res.keySet(), hasItems(KeyTypes.SIGNING.value()));
|
||||
assertThat(res.get(ConfigXmlConstants.SIGNING_ATTR), notNullValue());
|
||||
assertThat(res.get(ConfigXmlConstants.SIGNING_ATTR).size(), equalTo(2));
|
||||
|
||||
KeyInfo ki;
|
||||
KeyName keyName;
|
||||
X509Data x509data;
|
||||
X509Certificate x509certificate;
|
||||
|
||||
ki = res.get(ConfigXmlConstants.SIGNING_ATTR).get(0);
|
||||
assertThat(ki.getContent().size(), equalTo(2));
|
||||
assertThat((List<Object>) ki.getContent(), hasItem(instanceOf(X509Data.class)));
|
||||
assertThat((List<Object>) ki.getContent(), hasItem(instanceOf(KeyName.class)));
|
||||
|
||||
keyName = getContent(ki.getContent(), KeyName.class);
|
||||
assertThat(keyName.getName(), equalTo("rJkJlvowmv1Id74GznieaAC5jU5QQp_ILzuG-GsweTI"));
|
||||
|
||||
x509data = getContent(ki.getContent(), X509Data.class);
|
||||
assertThat(x509data, notNullValue());
|
||||
x509certificate = getContent(x509data.getContent(), X509Certificate.class);
|
||||
assertThat(x509certificate, notNullValue());
|
||||
assertThat(x509certificate.getSigAlgName(), equalTo("SHA256withRSA"));
|
||||
|
||||
ki = res.get(ConfigXmlConstants.SIGNING_ATTR).get(1);
|
||||
assertThat(ki.getContent().size(), equalTo(2));
|
||||
assertThat((List<Object>) ki.getContent(), hasItem(instanceOf(X509Data.class)));
|
||||
assertThat((List<Object>) ki.getContent(), hasItem(instanceOf(KeyName.class)));
|
||||
|
||||
keyName = getContent(ki.getContent(), KeyName.class);
|
||||
assertThat(keyName.getName(), equalTo("BzYc4GwL8HVrAhNyNdp-lTah2DvU9jU03kby9Ynohr4"));
|
||||
|
||||
x509data = getContent(ki.getContent(), X509Data.class);
|
||||
assertThat(x509data, notNullValue());
|
||||
x509certificate = getContent(x509data.getContent(), X509Certificate.class);
|
||||
assertThat(x509certificate, notNullValue());
|
||||
assertThat(x509certificate.getSigAlgName(), equalTo("SHA256withRSA"));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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.saml.config.parsers;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.adapters.saml.config.IDP;
|
||||
import org.keycloak.adapters.saml.config.Key;
|
||||
import org.keycloak.adapters.saml.config.KeycloakSamlAdapter;
|
||||
import org.keycloak.adapters.saml.config.SP;
|
||||
import org.keycloak.saml.common.util.StaxParserUtil;
|
||||
|
||||
import java.io.InputStream;
|
||||
import org.junit.Rule;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class KeycloakSamlAdapterXMLParserTest {
|
||||
|
||||
private static final String CURRENT_XSD_LOCATION = "/schema/keycloak_saml_adapter_1_7.xsd";
|
||||
|
||||
@Rule
|
||||
public ExpectedException expectedException = ExpectedException.none();
|
||||
|
||||
private void testValidationValid(String fileName) throws Exception {
|
||||
InputStream schema = getClass().getResourceAsStream(CURRENT_XSD_LOCATION);
|
||||
InputStream is = getClass().getResourceAsStream(fileName);
|
||||
assertNotNull(is);
|
||||
assertNotNull(schema);
|
||||
StaxParserUtil.validate(is, schema);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidationSimpleFile() throws Exception {
|
||||
testValidationValid("keycloak-saml.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidationMultipleKeys() throws Exception {
|
||||
testValidationValid("keycloak-saml-multiple-signing-keys.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidationWithHttpClient() throws Exception {
|
||||
testValidationValid("keycloak-saml-wth-http-client-settings.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidationKeyInvalid() throws Exception {
|
||||
InputStream schemaIs = KeycloakSamlAdapterXMLParser.class.getResourceAsStream(CURRENT_XSD_LOCATION);
|
||||
InputStream is = getClass().getResourceAsStream("keycloak-saml-invalid.xml");
|
||||
assertNotNull(is);
|
||||
assertNotNull(schemaIs);
|
||||
|
||||
expectedException.expect(ParsingException.class);
|
||||
StaxParserUtil.validate(is, schemaIs);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testXmlParser() throws Exception {
|
||||
InputStream is = getClass().getResourceAsStream("keycloak-saml.xml");
|
||||
assertNotNull(is);
|
||||
KeycloakSamlAdapterXMLParser parser = new KeycloakSamlAdapterXMLParser();
|
||||
|
||||
KeycloakSamlAdapter config = (KeycloakSamlAdapter)parser.parse(is);
|
||||
assertNotNull(config);
|
||||
assertEquals(1, config.getSps().size());
|
||||
SP sp = config.getSps().get(0);
|
||||
assertEquals("sp", sp.getEntityID());
|
||||
assertEquals("EXTERNAL", sp.getSslPolicy());
|
||||
assertEquals("format", sp.getNameIDPolicyFormat());
|
||||
assertTrue(sp.isForceAuthentication());
|
||||
assertTrue(sp.isIsPassive());
|
||||
assertEquals(2, sp.getKeys().size());
|
||||
Key signing = sp.getKeys().get(0);
|
||||
assertTrue(signing.isSigning());
|
||||
Key.KeyStoreConfig keystore = signing.getKeystore();
|
||||
assertNotNull(keystore);
|
||||
assertEquals("file", keystore.getFile());
|
||||
assertEquals("cp", keystore.getResource());
|
||||
assertEquals("pw", keystore.getPassword());
|
||||
assertEquals("private alias", keystore.getPrivateKeyAlias());
|
||||
assertEquals("private pw", keystore.getPrivateKeyPassword());
|
||||
assertEquals("cert alias", keystore.getCertificateAlias());
|
||||
Key encryption = sp.getKeys().get(1);
|
||||
assertTrue(encryption.isEncryption());
|
||||
assertEquals("private pem", encryption.getPrivateKeyPem());
|
||||
assertEquals("public pem", encryption.getPublicKeyPem());
|
||||
assertEquals("FROM_ATTRIBUTE", sp.getPrincipalNameMapping().getPolicy());
|
||||
assertEquals("attribute", sp.getPrincipalNameMapping().getAttributeName());
|
||||
assertTrue(sp.getRoleAttributes().size() == 1);
|
||||
assertTrue(sp.getRoleAttributes().contains("member"));
|
||||
|
||||
IDP idp = sp.getIdp();
|
||||
assertEquals("idp", idp.getEntityID());
|
||||
assertEquals("RSA_SHA256", idp.getSignatureAlgorithm());
|
||||
assertEquals("canon", idp.getSignatureCanonicalizationMethod());
|
||||
assertTrue(idp.getSingleSignOnService().isSignRequest());
|
||||
assertTrue(idp.getSingleSignOnService().isValidateResponseSignature());
|
||||
assertEquals("POST", idp.getSingleSignOnService().getRequestBinding());
|
||||
assertEquals("url", idp.getSingleSignOnService().getBindingUrl());
|
||||
|
||||
assertTrue(idp.getSingleLogoutService().isSignRequest());
|
||||
assertTrue(idp.getSingleLogoutService().isSignResponse());
|
||||
assertTrue(idp.getSingleLogoutService().isValidateRequestSignature());
|
||||
assertTrue(idp.getSingleLogoutService().isValidateResponseSignature());
|
||||
assertEquals("REDIRECT", idp.getSingleLogoutService().getRequestBinding());
|
||||
assertEquals("POST", idp.getSingleLogoutService().getResponseBinding());
|
||||
assertEquals("posturl", idp.getSingleLogoutService().getPostBindingUrl());
|
||||
assertEquals("redirecturl", idp.getSingleLogoutService().getRedirectBindingUrl());
|
||||
|
||||
assertTrue(idp.getKeys().size() == 1);
|
||||
assertTrue(idp.getKeys().get(0).isSigning());
|
||||
assertEquals("cert pem", idp.getKeys().get(0).getCertificatePem());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testXmlParserMultipleSigningKeys() throws Exception {
|
||||
InputStream is = getClass().getResourceAsStream("keycloak-saml-multiple-signing-keys.xml");
|
||||
assertNotNull(is);
|
||||
KeycloakSamlAdapterXMLParser parser = new KeycloakSamlAdapterXMLParser();
|
||||
|
||||
KeycloakSamlAdapter config = (KeycloakSamlAdapter) parser.parse(is);
|
||||
assertNotNull(config);
|
||||
assertEquals(1, config.getSps().size());
|
||||
SP sp = config.getSps().get(0);
|
||||
IDP idp = sp.getIdp();
|
||||
|
||||
assertTrue(idp.getKeys().size() == 4);
|
||||
for (int i = 0; i < 4; i ++) {
|
||||
Key key = idp.getKeys().get(i);
|
||||
assertTrue(key.isSigning());
|
||||
assertEquals("cert pem " + i, idp.getKeys().get(i).getCertificatePem());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testXmlParserHttpClientSettings() throws Exception {
|
||||
InputStream is = getClass().getResourceAsStream("keycloak-saml-wth-http-client-settings.xml");
|
||||
assertNotNull(is);
|
||||
KeycloakSamlAdapterXMLParser parser = new KeycloakSamlAdapterXMLParser();
|
||||
|
||||
KeycloakSamlAdapter config = (KeycloakSamlAdapter) parser.parse(is);
|
||||
assertNotNull(config);
|
||||
assertEquals(1, config.getSps().size());
|
||||
SP sp = config.getSps().get(0);
|
||||
IDP idp = sp.getIdp();
|
||||
|
||||
assertThat(idp.getHttpClientConfig(), notNullValue());
|
||||
assertThat(idp.getHttpClientConfig().getClientKeystore(), is("ks"));
|
||||
assertThat(idp.getHttpClientConfig().getClientKeystorePassword(), is("ks-pwd"));
|
||||
assertThat(idp.getHttpClientConfig().getProxyUrl(), is("pu"));
|
||||
assertThat(idp.getHttpClientConfig().getTruststore(), is("ts"));
|
||||
assertThat(idp.getHttpClientConfig().getTruststorePassword(), is("tsp"));
|
||||
assertThat(idp.getHttpClientConfig().getConnectionPoolSize(), is(42));
|
||||
assertThat(idp.getHttpClientConfig().isAllowAnyHostname(), is(true));
|
||||
assertThat(idp.getHttpClientConfig().isDisableTrustManager(), is(true));
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,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.
|
||||
*/
|
||||
|
||||
package org.keycloak.test.adapters.saml;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.adapters.saml.config.IDP;
|
||||
import org.keycloak.adapters.saml.config.Key;
|
||||
import org.keycloak.adapters.saml.config.KeycloakSamlAdapter;
|
||||
import org.keycloak.adapters.saml.config.SP;
|
||||
import org.keycloak.adapters.saml.config.parsers.KeycloakSamlAdapterXMLParser;
|
||||
import org.keycloak.saml.common.util.StaxParserUtil;
|
||||
|
||||
import javax.xml.XMLConstants;
|
||||
import javax.xml.transform.stream.StreamSource;
|
||||
import javax.xml.validation.Schema;
|
||||
import javax.xml.validation.SchemaFactory;
|
||||
import javax.xml.validation.Validator;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class XmlParserTest {
|
||||
|
||||
@Test
|
||||
public void testValidation() throws Exception {
|
||||
{
|
||||
InputStream schema = KeycloakSamlAdapterXMLParser.class.getResourceAsStream("/schema/keycloak_saml_adapter_1_6.xsd");
|
||||
InputStream is = getClass().getResourceAsStream("/keycloak-saml.xml");
|
||||
Assert.assertNotNull(is);
|
||||
Assert.assertNotNull(schema);
|
||||
StaxParserUtil.validate(is, schema);
|
||||
}
|
||||
{
|
||||
InputStream sch = KeycloakSamlAdapterXMLParser.class.getResourceAsStream("/schema/keycloak_saml_adapter_1_6.xsd");
|
||||
InputStream doc = getClass().getResourceAsStream("/keycloak-saml2.xml");
|
||||
Assert.assertNotNull(doc);
|
||||
Assert.assertNotNull(sch);
|
||||
try {
|
||||
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
|
||||
Schema schema = factory.newSchema(new StreamSource(sch));
|
||||
Validator validator = schema.newValidator();
|
||||
StreamSource source = new StreamSource(doc);
|
||||
source.setSystemId("/keycloak-saml2.xml");
|
||||
validator.validate(source);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testXmlParser() throws Exception {
|
||||
InputStream is = getClass().getResourceAsStream("/keycloak-saml.xml");
|
||||
Assert.assertNotNull(is);
|
||||
KeycloakSamlAdapterXMLParser parser = new KeycloakSamlAdapterXMLParser();
|
||||
KeycloakSamlAdapter config = (KeycloakSamlAdapter)parser.parse(is);
|
||||
Assert.assertNotNull(config);
|
||||
Assert.assertEquals(1, config.getSps().size());
|
||||
SP sp = config.getSps().get(0);
|
||||
Assert.assertEquals("sp", sp.getEntityID());
|
||||
Assert.assertEquals("ssl", sp.getSslPolicy());
|
||||
Assert.assertEquals("format", sp.getNameIDPolicyFormat());
|
||||
Assert.assertTrue(sp.isForceAuthentication());
|
||||
Assert.assertTrue(sp.isIsPassive());
|
||||
Assert.assertEquals(2, sp.getKeys().size());
|
||||
Key signing = sp.getKeys().get(0);
|
||||
Assert.assertTrue(signing.isSigning());
|
||||
Key.KeyStoreConfig keystore = signing.getKeystore();
|
||||
Assert.assertNotNull(keystore);
|
||||
Assert.assertEquals("file", keystore.getFile());
|
||||
Assert.assertEquals("cp", keystore.getResource());
|
||||
Assert.assertEquals("pw", keystore.getPassword());
|
||||
Assert.assertEquals("private alias", keystore.getPrivateKeyAlias());
|
||||
Assert.assertEquals("private pw", keystore.getPrivateKeyPassword());
|
||||
Assert.assertEquals("cert alias", keystore.getCertificateAlias());
|
||||
Key encryption = sp.getKeys().get(1);
|
||||
Assert.assertTrue(encryption.isEncryption());
|
||||
Assert.assertEquals("private pem", encryption.getPrivateKeyPem());
|
||||
Assert.assertEquals("public pem", encryption.getPublicKeyPem());
|
||||
Assert.assertEquals("policy", sp.getPrincipalNameMapping().getPolicy());
|
||||
Assert.assertEquals("attribute", sp.getPrincipalNameMapping().getAttributeName());
|
||||
Assert.assertTrue(sp.getRoleAttributes().size() == 1);
|
||||
Assert.assertTrue(sp.getRoleAttributes().contains("member"));
|
||||
|
||||
IDP idp = sp.getIdp();
|
||||
Assert.assertEquals("idp", idp.getEntityID());
|
||||
Assert.assertEquals("RSA", idp.getSignatureAlgorithm());
|
||||
Assert.assertEquals("canon", idp.getSignatureCanonicalizationMethod());
|
||||
Assert.assertTrue(idp.getSingleSignOnService().isSignRequest());
|
||||
Assert.assertTrue(idp.getSingleSignOnService().isValidateResponseSignature());
|
||||
Assert.assertEquals("post", idp.getSingleSignOnService().getRequestBinding());
|
||||
Assert.assertEquals("url", idp.getSingleSignOnService().getBindingUrl());
|
||||
|
||||
Assert.assertTrue(idp.getSingleLogoutService().isSignRequest());
|
||||
Assert.assertTrue(idp.getSingleLogoutService().isSignResponse());
|
||||
Assert.assertTrue(idp.getSingleLogoutService().isValidateRequestSignature());
|
||||
Assert.assertTrue(idp.getSingleLogoutService().isValidateResponseSignature());
|
||||
Assert.assertEquals("redirect", idp.getSingleLogoutService().getRequestBinding());
|
||||
Assert.assertEquals("post", idp.getSingleLogoutService().getResponseBinding());
|
||||
Assert.assertEquals("posturl", idp.getSingleLogoutService().getPostBindingUrl());
|
||||
Assert.assertEquals("redirecturl", idp.getSingleLogoutService().getRedirectBindingUrl());
|
||||
|
||||
Assert.assertTrue(idp.getKeys().size() == 1);
|
||||
Assert.assertTrue(idp.getKeys().get(0).isSigning());
|
||||
Assert.assertEquals("cert pem", idp.getKeys().get(0).getCertificatePem());
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
|
||||
~ 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.
|
||||
|
||||
-->
|
||||
<EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" Name="urn:keycloak">
|
||||
<EntityDescriptor entityID="http://localhost:8081/auth/realms/master">
|
||||
<IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<KeyDescriptor use="signing">
|
||||
<dsig:KeyInfo>
|
||||
<dsig:KeyName>rJkJlvowmv1Id74GznieaAC5jU5QQp_ILzuG-GsweTI</dsig:KeyName>
|
||||
<dsig:X509Data>
|
||||
<dsig:X509Certificate>
|
||||
MIICmzCCAYMCBgFX/9ccIDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMTYxMDI2MDcxMjUwWhcNMjYxMDI2MDgxNDMwWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPjDrM890OoFWLIU5xNT+v8B8EkpOGY1y/9Yi/yQd95uG/p5LaywiPsw+lPy4tSn1pH/2SxNDST2zynKPDd1lYDev43m0sC2FfD2H73q3udQRqSOxW1e8FrTrGDIHxb82UNrCPlu+fH+xYSkigrkOvLvPigTwSIcu8vgs0lk9FqJ81ty3Wj2e9lS7JJGAJ3pC7rp39VLdJSKbfyj/v2RYBeG5Pscncl8cjUOHUq5u19hThjkU2jOBzgIK2JS0bNmzSfH1eBTZMoCQBI1UJ1IbA8tqjQwpOXc+JkPBRU8T/JUQoQlSR6DTcPFvDgH2oGZYFHFfUontZqtz8jrIt2pxBAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAK5VgQp1x1FKgabFI6W/iGuy9ZCRoAixOOEGGkDps6dOEFgTQKTy5D/FZts9KuNxhhiD+NvS0d5BKYa5ITPLVPnGucgYkZhz+/+GhxmbjeQr0eJPaY7ZgLfH3tPA6tfdIkA0iE1En1sKEwt6R6DZjh9jtP9laoUoddTvYaFLJpZ2u1Ik94q6ZqX0fS/RKchaBHjhg6MtqCcHt07CBKHh8XNmKPXVSJC/p0MjyXv+qLaNNqyaAvAw6P6DX1hNjzrdkuaaHGXhu6kkezZUVlDWAm9cd1ppqalSK6ggy7yMW1NWTd/NYOPsFU2TS8DDPzRo14s1Qvw4v+TY6yT0NURJPQA=
|
||||
</dsig:X509Certificate>
|
||||
</dsig:X509Data>
|
||||
</dsig:KeyInfo>
|
||||
</KeyDescriptor>
|
||||
<KeyDescriptor use="signing">
|
||||
<dsig:KeyInfo>
|
||||
<dsig:KeyName>BzYc4GwL8HVrAhNyNdp-lTah2DvU9jU03kby9Ynohr4</dsig:KeyName>
|
||||
<dsig:X509Data>
|
||||
<dsig:X509Certificate>
|
||||
MIICmzCCAYMCBgFX/9eK7TANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMTYxMDI2MDcxMzE4WhcNMjYxMDI2MDgxNDU4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCDLT+40/BWzWPSVmpaSaZRs5lBMQ9VP9TCoXkby4PHqxIWRecTPM8fcNkPNPE/tiR2tUIpMXPDzgXNFA/EMoB3V1OEVXPecjKtiZczdR6pi75CBx7PJ2fSXg6xpjhZmHu0k7x591GZdP8Iiu2E6b9QA2p5VXgNgfuP07XzgabnSvIrLG60Imus3u6C2qA/QEuY7EYQWrFooriYLW6B8s3xU8R1a92SLMT8JsfMWXi+1CzAhIbVvdwUwkhVDDhAU6pUek88QQgxodd3FAMksoijCGFN1yrCkovlFhKb3j9AC6Icd9eeJuwYddN/nMeMGEDOeCcAGBACiaUisjUvZDw1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHAHbBI0CRfdw5ZHxHAjgSQvSj41c/4cfwln4Q7X3I5lMBbW3tcgond6Ku9eU46FzG5VpgXIgvEf4u0O9jUnxLlO50+t2SHwQ1RwHdBWQngVSZCRzscq3KrSzx1hx88qLyqcPrr3QtR92fYipDjENxttT/qJtDMrXlwLZEITlHDoneX319USYB9C4zlrCIsQ5XxQTTyCx886Pz15DSVSRxVp61HGk6ROsX/DG5/xwInlzgMZ0r3JWnAjtAaXqUrcwH9FXxco+xkiqKW79bGhWGQI9sXXvQSSNAaENMIUhxtd9uOi1l5e0EkKHE2fHlYyfdUDnFJWwSMXd/NM+hVI4Lw=
|
||||
</dsig:X509Certificate>
|
||||
</dsig:X509Data>
|
||||
</dsig:KeyInfo>
|
||||
</KeyDescriptor>
|
||||
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/auth/realms/master/protocol/saml"/>
|
||||
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:8081/auth/realms/master/protocol/saml"/>
|
||||
<NameIDFormat>
|
||||
urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
|
||||
</NameIDFormat>
|
||||
<NameIDFormat>
|
||||
urn:oasis:names:tc:SAML:2.0:nameid-format:transient
|
||||
</NameIDFormat>
|
||||
<NameIDFormat>
|
||||
urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
|
||||
</NameIDFormat>
|
||||
<NameIDFormat>
|
||||
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
|
||||
</NameIDFormat>
|
||||
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/auth/realms/master/protocol/saml"/>
|
||||
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:8081/auth/realms/master/protocol/saml"/>
|
||||
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://localhost:8081/auth/realms/master/protocol/saml"/>
|
||||
</IDPSSODescriptor>
|
||||
</EntityDescriptor>
|
||||
</EntitiesDescriptor>
|
||||
Loaded 100 of 1962 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user