Compare commits
1578
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b46cd7a6c6 | ||
|
|
f357310f40 | ||
|
|
f21d620e02 | ||
|
|
4ba7846ede | ||
|
|
0a39521581 | ||
|
|
030786ce55 | ||
|
|
6287e89acd | ||
|
|
ad64f42266 | ||
|
|
53d6953c9f | ||
|
|
b2f9775da5 | ||
|
|
40bb68e265 | ||
|
|
dacb1ebd50 | ||
|
|
063f2aa486 | ||
|
|
4f9e641fda | ||
|
|
1f9ec6a907 | ||
|
|
2648d0f1c5 | ||
|
|
1dfe480c59 | ||
|
|
6b242b26a9 | ||
|
|
7b77cd7d1f | ||
|
|
8a0c53bf1d | ||
|
|
a262d2263e | ||
|
|
bf548945db | ||
|
|
fd2c3996e2 | ||
|
|
ab81456fb3 | ||
|
|
e9445cfc20 | ||
|
|
75d715d8a7 | ||
|
|
62b90ee4e3 | ||
|
|
2f88868e20 | ||
|
|
b0de77013c | ||
|
|
25cd3e3ba8 | ||
|
|
1b0bf966b6 | ||
|
|
d8e28ad96b | ||
|
|
3336d4d7ac | ||
|
|
ee3a880a55 | ||
|
|
c9f9ee9799 | ||
|
|
0536f2c247 | ||
|
|
6f262423f1 | ||
|
|
3c5e7872a7 | ||
|
|
421dc6043d | ||
|
|
9402f89d8e | ||
|
|
81a0135fa6 | ||
|
|
43dc250bf2 | ||
|
|
bc3222ab67 | ||
|
|
e761d6ce10 | ||
|
|
46991764d4 | ||
|
|
747c0f9869 | ||
|
|
5424cd80a8 | ||
|
|
8d2becf681 | ||
|
|
c3f69fdc54 | ||
|
|
45215f361f | ||
|
|
802488cea0 | ||
|
|
3ac3d79e30 | ||
|
|
fe8755d4a4 | ||
|
|
e2b889d727 | ||
|
|
2ec4cb82cc | ||
|
|
bc14cade62 | ||
|
|
a6c852603e | ||
|
|
f1602b3b8d | ||
|
|
7c8bc5814e | ||
|
|
33f83689d5 | ||
|
|
80e2b8eb39 | ||
|
|
fbff61bfba | ||
|
|
ba1a17e668 | ||
|
|
7bc7510d1b | ||
|
|
dc337d684a | ||
|
|
0f9d50924c | ||
|
|
cf0eb1184a | ||
|
|
ecd31010fe | ||
|
|
0985d3c88a | ||
|
|
ddd99c2411 | ||
|
|
7834637749 | ||
|
|
a90d7e54d4 | ||
|
|
bae2b62200 | ||
|
|
71bcf697cc | ||
|
|
9acb4b86aa | ||
|
|
dee0be4237 | ||
|
|
c4544e8caf | ||
|
|
bca3893070 | ||
|
|
280725a2ae | ||
|
|
2892b093ab | ||
|
|
78fe064cf0 | ||
|
|
30aaea73e4 | ||
|
|
4b1b697aa3 | ||
|
|
112a44676d | ||
|
|
f7282d71e5 | ||
|
|
9840c47970 | ||
|
|
8695e16971 | ||
|
|
65f5ce9f13 | ||
|
|
77962ec896 | ||
|
|
54712e29aa | ||
|
|
71b6ed80ae | ||
|
|
4a4bbf26f4 | ||
|
|
64de96d34b | ||
|
|
084873f7fb | ||
|
|
c684dac625 | ||
|
|
a5c159eeff | ||
|
|
1cbe5c4c80 | ||
|
|
4a472b8272 | ||
|
|
ddf74c0d9c | ||
|
|
b2a3918fc3 | ||
|
|
aca00dd42c | ||
|
|
2a8e868bc1 | ||
|
|
979205c827 | ||
|
|
55e36acfc0 | ||
|
|
4b6825806a | ||
|
|
2d878805c1 | ||
|
|
dd038ddbd5 | ||
|
|
f6a02bd408 | ||
|
|
7ec0dad88f | ||
|
|
1e3ec1983d | ||
|
|
7ec02761ef | ||
|
|
7b30a6c9c4 | ||
|
|
0d56713871 | ||
|
|
658f204d92 | ||
|
|
04401af470 | ||
|
|
39d5a07218 | ||
|
|
a65f619c9a | ||
|
|
01c408ba68 | ||
|
|
63975886cb | ||
|
|
c3f2ae1cd1 | ||
|
|
133e4c59e5 | ||
|
|
d3247bc66a | ||
|
|
b6718b44a1 | ||
|
|
15506e3db3 | ||
|
|
977f4051b3 | ||
|
|
49c15f200e | ||
|
|
3e5874b2a1 | ||
|
|
f7ac5fae2a | ||
|
|
22ba4847f4 | ||
|
|
90822e5b34 | ||
|
|
3f62cee836 | ||
|
|
3bacbdf6ff | ||
|
|
edcc39d906 | ||
|
|
3ec516d14c | ||
|
|
d8d0298498 | ||
|
|
8356409b44 | ||
|
|
57971ce0b2 | ||
|
|
b708cd671e | ||
|
|
1b857387f2 | ||
|
|
cea70337ef | ||
|
|
6d5de7b44e | ||
|
|
10eba78036 | ||
|
|
dd6b05224d | ||
|
|
e7009cac47 | ||
|
|
eece3689f2 | ||
|
|
f48753f6e1 | ||
|
|
392ce0b323 | ||
|
|
2fce7f8498 | ||
|
|
c5f19ff2b3 | ||
|
|
b24dfa0009 | ||
|
|
a9b5d07263 | ||
|
|
eddf3eef17 | ||
|
|
dbac147419 | ||
|
|
6752a4f9b0 | ||
|
|
35804aa0df | ||
|
|
41d22986d5 | ||
|
|
9172b5472e | ||
|
|
b27b0411a6 | ||
|
|
0c293089c3 | ||
|
|
5403296ac6 | ||
|
|
20548b402d | ||
|
|
1747e0981f | ||
|
|
ea6374163d | ||
|
|
4782780857 | ||
|
|
b90409c5e4 | ||
|
|
9a921f66ff | ||
|
|
4a492ce13c | ||
|
|
39904548eb | ||
|
|
18268f686c | ||
|
|
1594e07628 | ||
|
|
606e6fa479 | ||
|
|
1afd3d422c | ||
|
|
80fae7116b | ||
|
|
86a0995f29 | ||
|
|
a7c684b909 | ||
|
|
5caf3e0a2d | ||
|
|
d939b6a431 | ||
|
|
42d17577d0 | ||
|
|
eaf326dd37 | ||
|
|
53a542d1e2 | ||
|
|
c51d5e9820 | ||
|
|
817127a751 | ||
|
|
0e6e3d10d8 | ||
|
|
0197c69ac3 | ||
|
|
83eea5036f | ||
|
|
1b614a379b | ||
|
|
0527d441e3 | ||
|
|
6569b22c79 | ||
|
|
d0ec1c3b02 | ||
|
|
2fe7e2a952 | ||
|
|
0d52e4e6c5 | ||
|
|
358c273d39 | ||
|
|
215d59b1e5 | ||
|
|
2b409b3462 | ||
|
|
b4e53b1677 | ||
|
|
051f175f68 | ||
|
|
fd447702e7 | ||
|
|
ee79f5e69d | ||
|
|
de5baa9df7 | ||
|
|
566a58b5d8 | ||
|
|
f837cbfaa9 | ||
|
|
06a55419f1 | ||
|
|
82a573a536 | ||
|
|
d46bad56ae | ||
|
|
32af1bd3a5 | ||
|
|
38b9d05cd7 | ||
|
|
e26e31d8b4 | ||
|
|
2328e0a6f9 | ||
|
|
96e1813b34 | ||
|
|
97357d020b | ||
|
|
b466cec4e5 | ||
|
|
9228c6a16b | ||
|
|
35f8e3c19f | ||
|
|
9956f425eb | ||
|
|
5464e600ea | ||
|
|
cbdba622a5 | ||
|
|
49e9965125 | ||
|
|
dcaf7ea9d5 | ||
|
|
3f51f620b7 | ||
|
|
06e44b185e | ||
|
|
17a2bd266b | ||
|
|
081db0d353 | ||
|
|
b82346761c | ||
|
|
595e7a92e2 | ||
|
|
fee1dea3e6 | ||
|
|
9a58e1276a | ||
|
|
1d401767d6 | ||
|
|
68e3b6c504 | ||
|
|
813ddf365e | ||
|
|
edf3499662 | ||
|
|
df26caed07 | ||
|
|
9f9067663f | ||
|
|
0f3d8b8513 | ||
|
|
4ca3bf530f | ||
|
|
c8c0a45ce4 | ||
|
|
7313b843b9 | ||
|
|
321cac2ab8 | ||
|
|
162dd4051d | ||
|
|
cd19c1acdf | ||
|
|
44ed912154 | ||
|
|
39856c32d1 | ||
|
|
d3a893b171 | ||
|
|
ba861f227e | ||
|
|
4a57bcc0b4 | ||
|
|
87a8e017fa | ||
|
|
b8fe608aaf | ||
|
|
d3e6b38a8e | ||
|
|
8589327cdd | ||
|
|
1b936a1230 | ||
|
|
45c490b640 | ||
|
|
1dd3fc537b | ||
|
|
73cf2404ff | ||
|
|
c16b6e6a43 | ||
|
|
8d23422605 | ||
|
|
9cd8d1612e | ||
|
|
277faea1d0 | ||
|
|
58c7b6090b | ||
|
|
f45bb62fcc | ||
|
|
34c3ffaae1 | ||
|
|
a218cc3034 | ||
|
|
c62b3822f4 | ||
|
|
b0e9720f88 | ||
|
|
6507f58b49 | ||
|
|
46e2440d30 | ||
|
|
9b51b6e43b | ||
|
|
4772b8401a | ||
|
|
2b25c120d4 | ||
|
|
c19e65f93c | ||
|
|
87994e032d | ||
|
|
ee6753d31b | ||
|
|
09c18306c2 | ||
|
|
6124fd64f3 | ||
|
|
2a03d23098 | ||
|
|
ff806eae08 | ||
|
|
e44d6f9888 | ||
|
|
4e79dde17f | ||
|
|
29e05ecf0d | ||
|
|
a388782f9b | ||
|
|
352ea30d20 | ||
|
|
6e0444e17b | ||
|
|
b3f0dc7399 | ||
|
|
ad34d63cb3 | ||
|
|
703720fa43 | ||
|
|
e01c789a93 | ||
|
|
8064c88e3c | ||
|
|
b06e747d28 | ||
|
|
f9b89df326 | ||
|
|
778a96c9e4 | ||
|
|
f6f6b6a258 | ||
|
|
c4f3bd3dda | ||
|
|
9d5356a001 | ||
|
|
32fb604c88 | ||
|
|
b3ea8f3add | ||
|
|
382161c425 | ||
|
|
ec527da868 | ||
|
|
0038c1ce9a | ||
|
|
d8fb0f1540 | ||
|
|
866c11b25b | ||
|
|
64f5d032ed | ||
|
|
635310123e | ||
|
|
0bf53ad0ab | ||
|
|
bf7863b300 | ||
|
|
07e12f3b5f | ||
|
|
0f3d2bbfb4 | ||
|
|
ed30c4454a | ||
|
|
0d4771a763 | ||
|
|
a8317e0cc2 | ||
|
|
5b61a10b55 | ||
|
|
ec327c99f4 | ||
|
|
0492d84b59 | ||
|
|
6a7a823fb4 | ||
|
|
f394e2552a | ||
|
|
5a686c4260 | ||
|
|
642e93be93 | ||
|
|
2a6851b01f | ||
|
|
2726db0be9 | ||
|
|
61c2ec04a4 | ||
|
|
8d6a1d5a1d | ||
|
|
233b1e2553 | ||
|
|
956ea431c8 | ||
|
|
265b6d5e2a | ||
|
|
b22c28b20e | ||
|
|
243acdd04e | ||
|
|
c49394c308 | ||
|
|
fd5cb84dd1 | ||
|
|
54b5a6c41e | ||
|
|
83fa03e8cc | ||
|
|
6863b711e3 | ||
|
|
2c5510284d | ||
|
|
96cfb895db | ||
|
|
46f947113c | ||
|
|
fa4e32ddcb | ||
|
|
57b60797ce | ||
|
|
38d160fab2 | ||
|
|
ef80b64d1c | ||
|
|
8d2e4c0316 | ||
|
|
ae83ad258b | ||
|
|
5fa59262ce | ||
|
|
373fc23fc0 | ||
|
|
58dc8776d6 | ||
|
|
20212d8d70 | ||
|
|
cb20d360c4 | ||
|
|
8e127bf699 | ||
|
|
4d24a874f9 | ||
|
|
7200996ae0 | ||
|
|
34cc750b13 | ||
|
|
0ad6d708da | ||
|
|
c83e3bd2d1 | ||
|
|
5ea880cfff | ||
|
|
4276328794 | ||
|
|
7c4c77d70e | ||
|
|
632216d53f | ||
|
|
d5105164e9 | ||
|
|
7a9e2db18f | ||
|
|
0d582a1326 | ||
|
|
a99df49bc9 | ||
|
|
b996e88dbd | ||
|
|
426a2b46fc | ||
|
|
4a020d545b | ||
|
|
ee363a4c56 | ||
|
|
bf4d5f4df9 | ||
|
|
30ec60b4e2 | ||
|
|
a744ecba9e | ||
|
|
e764461b1a | ||
|
|
f376facb48 | ||
|
|
31614df7c4 | ||
|
|
335f852600 | ||
|
|
c26aeb654b | ||
|
|
969cc6eb9e | ||
|
|
f5326e1f42 | ||
|
|
e664a4a6b2 | ||
|
|
c15571f65c | ||
|
|
1eacdc6904 | ||
|
|
be040eaa18 | ||
|
|
889679c715 | ||
|
|
8ae3551c4d | ||
|
|
cfd4de033b | ||
|
|
d6e2bccb16 | ||
|
|
eeec89583e | ||
|
|
d2bdb74fef | ||
|
|
b04b124351 | ||
|
|
067cb120f8 | ||
|
|
0bab4e63fa | ||
|
|
b48f32380c | ||
|
|
0c8f3f734d | ||
|
|
4f401c7547 | ||
|
|
df74338cbc | ||
|
|
e3060e5e58 | ||
|
|
15309a94cc | ||
|
|
2e39221b98 | ||
|
|
c05e21c049 | ||
|
|
62d519ed37 | ||
|
|
18fa03bf97 | ||
|
|
0bdb05e152 | ||
|
|
09af45201b | ||
|
|
dd85c125cb | ||
|
|
274838aa64 | ||
|
|
ecb39cda0c | ||
|
|
3685a185d4 | ||
|
|
030a35476a | ||
|
|
4ffe86583b | ||
|
|
24f765ceef | ||
|
|
c729835c3e | ||
|
|
507fa4c250 | ||
|
|
bba781beee | ||
|
|
980a1eca5e | ||
|
|
ff63c5552a | ||
|
|
985661c1b9 | ||
|
|
b168656eab | ||
|
|
94ba87ac05 | ||
|
|
cfc28b861b | ||
|
|
38408658f9 | ||
|
|
998bb9986f | ||
|
|
8d2a95d66b | ||
|
|
500bced6ee | ||
|
|
81960a6996 | ||
|
|
ccd456dba6 | ||
|
|
bac39bc71e | ||
|
|
a8c2e79565 | ||
|
|
d6089597ff | ||
|
|
c901fc7480 | ||
|
|
4f2b97de7f | ||
|
|
9a3e023c43 | ||
|
|
142ef50728 | ||
|
|
d2ba4669ab | ||
|
|
ba0b59bfd0 | ||
|
|
f1c3295cec | ||
|
|
04ddf7ba4d | ||
|
|
8f346a5f59 | ||
|
|
22e3abdaa8 | ||
|
|
f1af3e9fb7 | ||
|
|
5e479ba4df | ||
|
|
f6743eff69 | ||
|
|
5a761f2f3a | ||
|
|
261d32f8e7 | ||
|
|
282e702ef6 | ||
|
|
cc014fe0e5 | ||
|
|
95f62901c2 | ||
|
|
cf8b0abaad | ||
|
|
e761fb24e6 | ||
|
|
b1deed67df | ||
|
|
d3cadc9a47 | ||
|
|
32e069eadc | ||
|
|
17c97c2f68 | ||
|
|
48bb4d86e0 | ||
|
|
eda018ba0a | ||
|
|
ac1baa059f | ||
|
|
670b620da7 | ||
|
|
1545593cde | ||
|
|
aedd23a43d | ||
|
|
7e8c80c0df | ||
|
|
9f6813fcad | ||
|
|
14292843ff | ||
|
|
71e25bcd65 | ||
|
|
9f1fa5ebb9 | ||
|
|
98958a2bc4 | ||
|
|
7da9f8085d | ||
|
|
6989589e72 | ||
|
|
4f00f6cceb | ||
|
|
95e729b0b7 | ||
|
|
38c460de82 | ||
|
|
41331111da | ||
|
|
bff334d365 | ||
|
|
41c9289f14 | ||
|
|
1d06d9dd64 | ||
|
|
ec9b2933cc | ||
|
|
1ff5d6eb06 | ||
|
|
bc22abf8cb | ||
|
|
81eb1e1e30 | ||
|
|
e8cb3a416d | ||
|
|
764c20d748 | ||
|
|
c81d0c0898 | ||
|
|
67fca8f1f3 | ||
|
|
22c5c803ca | ||
|
|
f6d716a386 | ||
|
|
62c5bc0e91 | ||
|
|
5e329d7b29 | ||
|
|
1bd9b18803 | ||
|
|
96fe4269ec | ||
|
|
83ff02ea53 | ||
|
|
1df741a307 | ||
|
|
409356f734 | ||
|
|
bad0a95123 | ||
|
|
1bc08fff16 | ||
|
|
ab436705c8 | ||
|
|
ea7709a104 | ||
|
|
1d5a01577e | ||
|
|
4288260aa6 | ||
|
|
600d1f8a56 | ||
|
|
f7b1af7e11 | ||
|
|
4b0e33e388 | ||
|
|
8caaabb2e5 | ||
|
|
1bd0e23f31 | ||
|
|
13a52c1bb2 | ||
|
|
fa7a7d35a9 | ||
|
|
fe5809db4d | ||
|
|
1510d9672a | ||
|
|
9b93468131 | ||
|
|
2e16a55815 | ||
|
|
f65665b5eb | ||
|
|
ce4a865579 | ||
|
|
f2e4e67ba0 | ||
|
|
37f1eaae0e | ||
|
|
6c3a3517b8 | ||
|
|
c4416a25e0 | ||
|
|
550f773293 | ||
|
|
d7ea66ad44 | ||
|
|
2022a8d36b | ||
|
|
0d20e3c7ff | ||
|
|
21119604c6 | ||
|
|
973609d39b | ||
|
|
77bccda7a4 | ||
|
|
0c9f0ea3e6 | ||
|
|
73197332c4 | ||
|
|
ee0dd7e200 | ||
|
|
9203971809 | ||
|
|
4e7fe1de8d | ||
|
|
4646a4aceb | ||
|
|
64baa28301 | ||
|
|
db84571546 | ||
|
|
99cddcef6c | ||
|
|
1891019067 | ||
|
|
b0b4d26fa3 | ||
|
|
6cc76b2e3d | ||
|
|
33ac048c8c | ||
|
|
e25157655b | ||
|
|
213abb87ba | ||
|
|
825a68c6c9 | ||
|
|
816ed9419b | ||
|
|
15f1a38b51 | ||
|
|
1f75f85a20 | ||
|
|
dfb39d13a2 | ||
|
|
b8391e0935 | ||
|
|
ab56460325 | ||
|
|
059ebacffc | ||
|
|
e1a07bd5a8 | ||
|
|
37d7182fc9 | ||
|
|
e1ba26bcae | ||
|
|
b39146f6f5 | ||
|
|
caea1a3458 | ||
|
|
7ef747e7c4 | ||
|
|
4ca442d1b2 | ||
|
|
99e75c69a0 | ||
|
|
1437f7da35 | ||
|
|
fa9abbf016 | ||
|
|
6bce32ba62 | ||
|
|
ee801c2c32 | ||
|
|
2b23497f92 | ||
|
|
04ba5c235e | ||
|
|
1521f56225 | ||
|
|
251adbba0b | ||
|
|
adbf2b22ad | ||
|
|
2b29c3acf4 | ||
|
|
076690e21e | ||
|
|
b586f93182 | ||
|
|
5a318df041 | ||
|
|
cd01ea49b4 | ||
|
|
dd2824ab20 | ||
|
|
8db6972441 | ||
|
|
027311c0c0 | ||
|
|
68edf9ce48 | ||
|
|
7b7fbd3257 | ||
|
|
d0c50fe56d | ||
|
|
4075d65241 | ||
|
|
267be83863 | ||
|
|
15828e899b | ||
|
|
ce089019e6 | ||
|
|
c9c49a9c64 | ||
|
|
623f39862d | ||
|
|
818c3ab1c5 | ||
|
|
1ce028ead7 | ||
|
|
d844e6b4af | ||
|
|
7b4dcb6965 | ||
|
|
7bd0b7ddfc | ||
|
|
d2912673b0 | ||
|
|
28ee95138a | ||
|
|
d46b4eb5fe | ||
|
|
151c56a304 | ||
|
|
c2ad918975 | ||
|
|
634eadb634 | ||
|
|
d26cfcda42 | ||
|
|
4a9a39c08d | ||
|
|
9796b7f06e | ||
|
|
c2e14cb924 | ||
|
|
d896800ec6 | ||
|
|
b74e0dad10 | ||
|
|
2c8b0fc3e1 | ||
|
|
cd640339fc | ||
|
|
52c02f03ae | ||
|
|
84da341912 | ||
|
|
2c3958ce27 | ||
|
|
515f049ce6 | ||
|
|
03acad9b9d | ||
|
|
0392ebab32 | ||
|
|
52804da497 | ||
|
|
31df2046f0 | ||
|
|
f14681f122 | ||
|
|
d7d52b3295 | ||
|
|
8e71910e00 | ||
|
|
e6620ca005 | ||
|
|
490b8ce86f | ||
|
|
480444ffc5 | ||
|
|
2fd7e5b355 | ||
|
|
f45e0aa387 | ||
|
|
6c77c295d5 | ||
|
|
ccfaf72b09 | ||
|
|
700740a378 | ||
|
|
8062f5f50e | ||
|
|
5e0851c25f | ||
|
|
81650a89f9 | ||
|
|
709449243c | ||
|
|
df24d8a01f | ||
|
|
f8f4de9389 | ||
|
|
9755d79879 | ||
|
|
70cbeaf336 | ||
|
|
b0a71fbeb0 | ||
|
|
44f56e1d74 | ||
|
|
74924f2f8c | ||
|
|
c498b06f68 | ||
|
|
525bee66a4 | ||
|
|
cd51a95764 | ||
|
|
57c282eca8 | ||
|
|
ebd02a1a7f | ||
|
|
cc4583bc0b | ||
|
|
11ef67722b | ||
|
|
5316dd6d50 | ||
|
|
3cee33847f | ||
|
|
a2b5b6dc71 | ||
|
|
a5dc91f9a6 | ||
|
|
3b4b1b20d8 | ||
|
|
b32423d96b | ||
|
|
4f2fe0197d | ||
|
|
aa25c899ee | ||
|
|
fa6b99eaab | ||
|
|
e04dbffb88 | ||
|
|
f823b17b90 | ||
|
|
7dfb7ddccd | ||
|
|
4b444d36b0 | ||
|
|
23546402fa | ||
|
|
edfc7ea103 | ||
|
|
3a7c84c9e1 | ||
|
|
6b1287a553 | ||
|
|
c860c671d1 | ||
|
|
072950e9f3 | ||
|
|
8226abef40 | ||
|
|
0d05d38eb6 | ||
|
|
4f4368b29f | ||
|
|
3f8312427a | ||
|
|
73bb579dc5 | ||
|
|
c75528f81e | ||
|
|
28bfac7214 | ||
|
|
1c9a347391 | ||
|
|
df03f51563 | ||
|
|
7d590761dd | ||
|
|
944c416b45 | ||
|
|
028cbe1ce2 | ||
|
|
256ca2b665 | ||
|
|
e4412523d4 | ||
|
|
073d82b367 | ||
|
|
484c5ebb15 | ||
|
|
4c33704c56 | ||
|
|
511763d4bc | ||
|
|
605c80a68d | ||
|
|
2325143803 | ||
|
|
9cc5ca3ce4 | ||
|
|
d13c120b6c | ||
|
|
186de7235c | ||
|
|
9433ef284a | ||
|
|
9942c7b519 | ||
|
|
4587fd23b6 | ||
|
|
32647d8682 | ||
|
|
d8ebf1e3d6 | ||
|
|
c21b56d596 | ||
|
|
4ee5c32b1e | ||
|
|
6386d95904 | ||
|
|
f99395aebe | ||
|
|
82a7173dba | ||
|
|
2bf6f4b0fd | ||
|
|
d8e90028af | ||
|
|
d2afb4892a | ||
|
|
eb9f69eee0 | ||
|
|
39f985e04a | ||
|
|
41a51d924b | ||
|
|
ce996f6a41 | ||
|
|
244848afc4 | ||
|
|
2910db5595 | ||
|
|
a6556a49c2 | ||
|
|
e4961056db | ||
|
|
c8eee38471 | ||
|
|
1e4b3fc3b6 | ||
|
|
14426f26f2 | ||
|
|
19ad846306 | ||
|
|
235ffb2ff6 | ||
|
|
181fdeb0d0 | ||
|
|
8674578d0d | ||
|
|
9cf4186eb2 | ||
|
|
92ed01a61b | ||
|
|
99028e9cab | ||
|
|
20f18eec15 | ||
|
|
67435791ed | ||
|
|
b4520baee5 | ||
|
|
802a39b1ce | ||
|
|
229b964867 | ||
|
|
b413bcb41d | ||
|
|
0c39e30241 | ||
|
|
11a8d3842e | ||
|
|
2c460c3adc | ||
|
|
aaddaa6c25 | ||
|
|
96099e0798 | ||
|
|
e582de2837 | ||
|
|
e1fc863421 | ||
|
|
7de4051c9b | ||
|
|
5b7b5b1b60 | ||
|
|
f8effaee58 | ||
|
|
bf7d43fffb | ||
|
|
870702fd81 | ||
|
|
2a3e5fa7a4 | ||
|
|
90063cafec | ||
|
|
2b6fcc05e1 | ||
|
|
f585ed5582 | ||
|
|
192387047f | ||
|
|
78664e2571 | ||
|
|
bc71739fb2 | ||
|
|
5563118d79 | ||
|
|
aadd63e2b0 | ||
|
|
59d548228a | ||
|
|
68c3f2f65b | ||
|
|
b54bb25121 | ||
|
|
7f65a2be16 | ||
|
|
b907f8f92c | ||
|
|
3381ed50d1 | ||
|
|
c2c2b835b2 | ||
|
|
f92fe6bea9 | ||
|
|
6aa029784a | ||
|
|
325072c74d | ||
|
|
1e48143db0 | ||
|
|
8c72ef2980 | ||
|
|
e7febbd5a5 | ||
|
|
d9211d6bc9 | ||
|
|
8121a9391f | ||
|
|
65b05fd4e4 | ||
|
|
940c9f8bf1 | ||
|
|
c85e0248dd | ||
|
|
2ec143a3ba | ||
|
|
a279ff3d7f | ||
|
|
128c26043b | ||
|
|
fe9b5a6588 | ||
|
|
0f93e72b02 | ||
|
|
bdba5ff8b1 | ||
|
|
7071161a7c | ||
|
|
672103e363 | ||
|
|
f579d7ffe9 | ||
|
|
d1f0b353e8 | ||
|
|
ef56dca050 | ||
|
|
07c3772b08 | ||
|
|
85a886da18 | ||
|
|
4ce93171ba | ||
|
|
c987d57b9e | ||
|
|
a416327124 | ||
|
|
285ef50341 | ||
|
|
0d22074826 | ||
|
|
b76fb68ff7 | ||
|
|
f03128f5ed | ||
|
|
8fb05bacd1 | ||
|
|
f8842b3df1 | ||
|
|
b376d1fb6d | ||
|
|
d4e0901599 | ||
|
|
217c359530 | ||
|
|
4fbf6841be | ||
|
|
2deb20a2c9 | ||
|
|
20b92c410f | ||
|
|
1d829a8365 | ||
|
|
10b4bd24a4 | ||
|
|
788501f13d | ||
|
|
345bea50b6 | ||
|
|
ab7bfa6d90 | ||
|
|
4f7979d9fc | ||
|
|
286458b94a | ||
|
|
61a8c422df | ||
|
|
baaf445727 | ||
|
|
2faf0eccdb | ||
|
|
639b5b617b | ||
|
|
115663a32d | ||
|
|
b277afa6d9 | ||
|
|
960cd8c822 | ||
|
|
4acc69c604 | ||
|
|
0230e008c5 | ||
|
|
382b8d60d0 | ||
|
|
0ad29c9737 | ||
|
|
9ced56d8d7 | ||
|
|
eb197c46f7 | ||
|
|
63947cd37a | ||
|
|
938ce9612c | ||
|
|
d6119c04d6 | ||
|
|
14646781ad | ||
|
|
1b41832946 | ||
|
|
57d0bac96a | ||
|
|
2ca3ce2dba | ||
|
|
e5d404fdb3 | ||
|
|
8829fe8658 | ||
|
|
9ccb0b1f65 | ||
|
|
7bb70f2d00 | ||
|
|
3ab7edbbcd | ||
|
|
d4060cce92 | ||
|
|
1342904fae | ||
|
|
fc0a3c3e73 | ||
|
|
756a9f095c | ||
|
|
021e48d6d6 | ||
|
|
91f78c61b7 | ||
|
|
d39aee0a72 | ||
|
|
7c600e2f4b | ||
|
|
14079b950a | ||
|
|
173c112f8e | ||
|
|
67d8ca0207 | ||
|
|
e690f4e645 | ||
|
|
4c554b4af6 | ||
|
|
3aee5eeecf | ||
|
|
366a1629e5 | ||
|
|
7fec1677ee | ||
|
|
8e391ffa4e | ||
|
|
d65ce5d1d4 | ||
|
|
6741dffe38 | ||
|
|
894af03323 | ||
|
|
80dea9c0bd | ||
|
|
0b83325470 | ||
|
|
368ea64b51 | ||
|
|
450e031da9 | ||
|
|
1796a45a68 | ||
|
|
c9437595b7 | ||
|
|
a1b315adbd | ||
|
|
1781588597 | ||
|
|
f29aff4bed | ||
|
|
ac91839e69 | ||
|
|
9c0c8e37b6 | ||
|
|
d40237b08d | ||
|
|
59527e12f0 | ||
|
|
6d0483667f | ||
|
|
dcf8747d7e | ||
|
|
7a3b4823b0 | ||
|
|
64ecc3b153 | ||
|
|
cc41a1fd78 | ||
|
|
255b1cb3bf | ||
|
|
a1bb202bd4 | ||
|
|
332e3f6099 | ||
|
|
6b41d79058 | ||
|
|
3b4b5c11e6 | ||
|
|
0c9e764e11 | ||
|
|
00cbc22d8b | ||
|
|
0a3de09b5a | ||
|
|
4e0ad5ac0b | ||
|
|
72256fd6ac | ||
|
|
d06237a3fd | ||
|
|
28fad68486 | ||
|
|
53b86cbace | ||
|
|
a53aebcddf | ||
|
|
af307c695a | ||
|
|
7816f053a6 | ||
|
|
ed54ba9064 | ||
|
|
8acdaf462b | ||
|
|
c5efd34a46 | ||
|
|
75343986b0 | ||
|
|
424d357c9c | ||
|
|
56116038d6 | ||
|
|
c76da8f3b8 | ||
|
|
589eb3206e | ||
|
|
5419ec32e4 | ||
|
|
c9e0a7a97d | ||
|
|
5b5f99fdfe | ||
|
|
d3bd0a67a7 | ||
|
|
2a36185419 | ||
|
|
76e3c6394c | ||
|
|
9e15b20591 | ||
|
|
05bd51ac1c | ||
|
|
d648ba73de | ||
|
|
c6b34f32a1 | ||
|
|
974c0a50e7 | ||
|
|
07d3b26722 | ||
|
|
510b03476f | ||
|
|
4f6d3c8dca | ||
|
|
95fe2fe74b | ||
|
|
e937216cf7 | ||
|
|
fd8d93663e | ||
|
|
a048cfc102 | ||
|
|
272272634d | ||
|
|
01947f46b9 | ||
|
|
5ec73a81e9 | ||
|
|
4cba764ac3 | ||
|
|
84b9963609 | ||
|
|
99c216635f | ||
|
|
25a6fcc382 | ||
|
|
acefd3cacd | ||
|
|
15a5c87cc5 | ||
|
|
3044138e10 | ||
|
|
6fbb8ccf71 | ||
|
|
018866aa81 | ||
|
|
18b2b572ac | ||
|
|
d9e67aa5ee | ||
|
|
e675ce44d5 | ||
|
|
df46a8ea0d | ||
|
|
e277d5b55e | ||
|
|
4bd1d899db | ||
|
|
3e3d92c2e4 | ||
|
|
55deedd3b8 | ||
|
|
8b6251eb23 | ||
|
|
35919da0a4 | ||
|
|
ec018d2739 | ||
|
|
b1a23a13d6 | ||
|
|
3ebbe96786 | ||
|
|
cfaa4c60c9 | ||
|
|
e104450fc8 | ||
|
|
c22b0f4da8 | ||
|
|
302d0b58cc | ||
|
|
72999f0acb | ||
|
|
a942322d99 | ||
|
|
8902502c4d | ||
|
|
8d19741f53 | ||
|
|
e60549d764 | ||
|
|
0a42a28eeb | ||
|
|
88355d7eb4 | ||
|
|
67db5917ac | ||
|
|
8f1daf5d77 | ||
|
|
6d5e3b2a17 | ||
|
|
75c0d5089f | ||
|
|
5b1fcbfc51 | ||
|
|
0c80255f3a | ||
|
|
121f8e9174 | ||
|
|
c1cb50b39f | ||
|
|
791a740f32 | ||
|
|
546cdd7d8f | ||
|
|
8ef6602f82 | ||
|
|
2938759db4 | ||
|
|
f7bcca8553 | ||
|
|
4eaf893492 | ||
|
|
0ba6ab198a | ||
|
|
49a530f2b4 | ||
|
|
c14d3d7963 | ||
|
|
fe26d6ecec | ||
|
|
35a00b449d | ||
|
|
1e9c09d23a | ||
|
|
ec4f8a7f15 | ||
|
|
d109bf7b56 | ||
|
|
4625ab917d | ||
|
|
7d239f8990 | ||
|
|
22f7efd977 | ||
|
|
2a349673b9 | ||
|
|
361019fc6a | ||
|
|
abb099745f | ||
|
|
046edbbd54 | ||
|
|
8cc4c8c5e2 | ||
|
|
d2d3a3f216 | ||
|
|
cbf5c920ea | ||
|
|
7ac1d1f14c | ||
|
|
e811b87679 | ||
|
|
d08fca4e0a | ||
|
|
30aa41a2ee | ||
|
|
9d8d690100 | ||
|
|
43541d3028 | ||
|
|
880e831e71 | ||
|
|
fe2278b320 | ||
|
|
f7a8bbbcc8 | ||
|
|
fde4bc99aa | ||
|
|
cc9d6d0cf7 | ||
|
|
b17bbc9b86 | ||
|
|
d4c5349e1a | ||
|
|
2865a3c047 | ||
|
|
522fc7412c | ||
|
|
7ec3f86efb | ||
|
|
c11539cccb | ||
|
|
09488b8df3 | ||
|
|
e16cc493d2 | ||
|
|
379fc8961e | ||
|
|
43114f176b | ||
|
|
f01d959f7c | ||
|
|
28f4409768 | ||
|
|
364aecda2a | ||
|
|
9bfbac6be5 | ||
|
|
db20fd9eeb | ||
|
|
53a198fde8 | ||
|
|
861a13501a | ||
|
|
0f24bd2ea4 | ||
|
|
9dc54815eb | ||
|
|
cb8ca619ae | ||
|
|
ac91deac96 | ||
|
|
71e2be5d3d | ||
|
|
4878073867 | ||
|
|
dc327ddddb | ||
|
|
e6467403ef | ||
|
|
cf0fab2d66 | ||
|
|
2eaa03539c | ||
|
|
b7e49dc88d | ||
|
|
ca05455aa0 | ||
|
|
7ae3eb8321 | ||
|
|
ae54babbbb | ||
|
|
a8378fe84b | ||
|
|
85df0b6a67 | ||
|
|
95967b9c79 | ||
|
|
8396b999a2 | ||
|
|
3bbe82057c | ||
|
|
0c49fce78f | ||
|
|
f9af8ba1a0 | ||
|
|
80f91b5fa5 | ||
|
|
6478e8f5c7 | ||
|
|
55dc42e147 | ||
|
|
f55ab0d689 | ||
|
|
fe79c9b01e | ||
|
|
6291e90f9c | ||
|
|
a32fefb597 | ||
|
|
3e04d045ee | ||
|
|
fb4645a3dc | ||
|
|
e0eac89e5a | ||
|
|
90d34bfd06 | ||
|
|
e58b5762f3 | ||
|
|
3fd4d23bed | ||
|
|
1c38bb7158 | ||
|
|
149ef706dd | ||
|
|
75bd8d4627 | ||
|
|
f1c40dd345 | ||
|
|
6225ccfc06 | ||
|
|
34de631b2a | ||
|
|
86f0092622 | ||
|
|
e455cbf319 | ||
|
|
cee2d69718 | ||
|
|
ff17123123 | ||
|
|
e118a06a09 | ||
|
|
4898d74c6d | ||
|
|
76f3842dad | ||
|
|
d818ebdd9e | ||
|
|
4d90898dd9 | ||
|
|
298b9e01d8 | ||
|
|
d0b08cacd8 | ||
|
|
050c65a520 | ||
|
|
be0c359160 | ||
|
|
3f792030d3 | ||
|
|
5c2fc1120b | ||
|
|
3d2acbe780 | ||
|
|
bce0e0ea90 | ||
|
|
931102ff26 | ||
|
|
5a3938d12f | ||
|
|
dca1751a63 | ||
|
|
f6ec9af61e | ||
|
|
64f2f84ffa | ||
|
|
5df5e53f11 | ||
|
|
81f4c50574 | ||
|
|
2550d16127 | ||
|
|
333ad0efac | ||
|
|
f3675681c3 | ||
|
|
28de8dc5b7 | ||
|
|
35e63a9398 | ||
|
|
f23f1cc813 | ||
|
|
b0095154d1 | ||
|
|
79ea9ed314 | ||
|
|
413db0dce1 | ||
|
|
e6745532ce | ||
|
|
59da0f90f8 | ||
|
|
76f0f52ee3 | ||
|
|
73c230f1ca | ||
|
|
ddee72c756 | ||
|
|
6318964374 | ||
|
|
853f1af40d | ||
|
|
d429b2151a | ||
|
|
b223e1a7af | ||
|
|
c806b8911e | ||
|
|
9f30a51925 | ||
|
|
a8b7129269 | ||
|
|
991b0e8ee2 | ||
|
|
e9d1475475 | ||
|
|
bc189554aa | ||
|
|
d0f26ae4cf | ||
|
|
ab6d247ef1 | ||
|
|
fcc82a47e4 | ||
|
|
25e53db96f | ||
|
|
89660d06e5 | ||
|
|
493fd0ad6a | ||
|
|
11422524f3 | ||
|
|
957214ed78 | ||
|
|
f013cca3b8 | ||
|
|
a1cfc0a0ff | ||
|
|
e8b3b3acf3 | ||
|
|
0cfb03be03 | ||
|
|
7d4b93e01e | ||
|
|
2f3793805b | ||
|
|
a310c13d32 | ||
|
|
38a721425d | ||
|
|
dfc34d99c5 | ||
|
|
6c58fd03a4 | ||
|
|
5b9d1286cc | ||
|
|
d9e330802c | ||
|
|
085de8a069 | ||
|
|
5af4c63a3e | ||
|
|
5177365c28 | ||
|
|
4df5ff3004 | ||
|
|
00f6841dc7 | ||
|
|
be8394158f | ||
|
|
fbf62b2a63 | ||
|
|
a047fb947b | ||
|
|
eec3a3d5c3 | ||
|
|
9387de1d1a | ||
|
|
b9c90f128a | ||
|
|
5699efc0f5 | ||
|
|
6ead008bf4 | ||
|
|
34e52839f7 | ||
|
|
bd54e1c60c | ||
|
|
03006522e2 | ||
|
|
7492ae2990 | ||
|
|
6edf890699 | ||
|
|
43fd24f02b | ||
|
|
22ebb81650 | ||
|
|
fd074278f7 | ||
|
|
ad497a66cd | ||
|
|
41275b0e91 | ||
|
|
b82502af1d | ||
|
|
43f2199d46 | ||
|
|
d44541fc9e | ||
|
|
24147a3d84 | ||
|
|
244d879d00 | ||
|
|
f5f67a629e | ||
|
|
aad1d57502 | ||
|
|
cf75c2378f | ||
|
|
3a64c4d582 | ||
|
|
d36376ffcd | ||
|
|
434197c10d | ||
|
|
d4af694c6f | ||
|
|
f245b67036 | ||
|
|
945673c7da | ||
|
|
c25967bd61 | ||
|
|
165fba6b9c | ||
|
|
76209dd899 | ||
|
|
8c8cc53989 | ||
|
|
6f463196ef | ||
|
|
b0e2624343 | ||
|
|
99fda3a7cb | ||
|
|
457039d7ba | ||
|
|
eb4ffbca29 | ||
|
|
c9afe8fcc4 | ||
|
|
d8d6348f67 | ||
|
|
87f7ec5909 | ||
|
|
18583eb6ed | ||
|
|
b9d0219f04 | ||
|
|
d9cb1fc4a9 | ||
|
|
1654be0a85 | ||
|
|
d5944163d6 | ||
|
|
5ca3a48094 | ||
|
|
1fb3d7cd3b | ||
|
|
f827b2dc0d | ||
|
|
0295824923 | ||
|
|
8fee0c946b | ||
|
|
bd13b8a5fe | ||
|
|
743ea95a78 | ||
|
|
2998a0fa35 | ||
|
|
7b0a3989e2 | ||
|
|
06fcb7595a | ||
|
|
bd3c4d4215 | ||
|
|
b969d9edfa | ||
|
|
3ef0fb63a9 | ||
|
|
bf706a9191 | ||
|
|
26be615a80 | ||
|
|
81928eb61a | ||
|
|
126f517520 | ||
|
|
7028496601 | ||
|
|
3a4897c7d2 | ||
|
|
b3f142d715 | ||
|
|
a4e16ca9c7 | ||
|
|
57176dee75 | ||
|
|
f87ba8a3a9 | ||
|
|
8c0b33f902 | ||
|
|
51e927adf7 | ||
|
|
bf4b681506 | ||
|
|
3c88d2cc18 | ||
|
|
c7b5975ac1 | ||
|
|
09901997c1 | ||
|
|
c0f3d851db | ||
|
|
98b6344f03 | ||
|
|
e4b6e4c167 | ||
|
|
d6983e829a | ||
|
|
374a2ad957 | ||
|
|
52fd2d7e75 | ||
|
|
25bb0ade1b | ||
|
|
6d7be80930 | ||
|
|
1f13f6372a | ||
|
|
39fcd84d53 | ||
|
|
d11a83d6e2 | ||
|
|
1978e23cfd | ||
|
|
4cc1a774df | ||
|
|
8a23463328 | ||
|
|
fad2913665 | ||
|
|
c4c27973f3 | ||
|
|
2ae733bd07 | ||
|
|
7665b9f81e | ||
|
|
248322c748 | ||
|
|
523bdd5ec9 | ||
|
|
a8ad00156b | ||
|
|
8f820713fa | ||
|
|
fc7d4b072c | ||
|
|
1801526f65 | ||
|
|
58e37b6a3f | ||
|
|
0bd9cad6ad | ||
|
|
bcc2c893ef | ||
|
|
ac71bbd4b5 | ||
|
|
ae94b30066 | ||
|
|
b3c307affe | ||
|
|
03d3f4229e | ||
|
|
c627b83cc5 | ||
|
|
5469db311d | ||
|
|
75793a36cf | ||
|
|
d68603e4ea | ||
|
|
33e402e7be | ||
|
|
375dce14e6 | ||
|
|
08ae3aee50 | ||
|
|
1ad7d4f1d8 | ||
|
|
f9e8f1f7b7 | ||
|
|
fa0b45625c | ||
|
|
c5459b2614 | ||
|
|
8232773d97 | ||
|
|
53b752ccfe | ||
|
|
07efba364e | ||
|
|
2e52d8ea7a | ||
|
|
a46a7a42a1 | ||
|
|
060a193b48 | ||
|
|
06ee06ae50 | ||
|
|
17a5a9f033 | ||
|
|
e27a754f5f | ||
|
|
61d5ba6a92 | ||
|
|
eff0054b4a | ||
|
|
99cd6c7383 | ||
|
|
5087392162 | ||
|
|
7474a31d95 | ||
|
|
a0197bc9da | ||
|
|
3b2e679aed | ||
|
|
75d69a1ec1 | ||
|
|
930fc66ebf | ||
|
|
04d3d26cb1 | ||
|
|
de940ccff9 | ||
|
|
aedc54cc89 | ||
|
|
de3a776b9a | ||
|
|
5d6cc61d4d | ||
|
|
f32b38cdbc | ||
|
|
f3bfb06dec | ||
|
|
630597746e | ||
|
|
f98036296c | ||
|
|
bf1f867f23 | ||
|
|
8215a85930 | ||
|
|
10a5686955 | ||
|
|
2e3c7a3acf | ||
|
|
06713da67b | ||
|
|
46fb1b6582 | ||
|
|
1b3f98db1e | ||
|
|
a190cdd8f2 | ||
|
|
8e25369001 | ||
|
|
8d90ad816a | ||
|
|
da9e42cb21 | ||
|
|
cbab3ff0d0 | ||
|
|
210b570c89 | ||
|
|
a2203df6a3 | ||
|
|
6a2b369014 | ||
|
|
fda6a6657f | ||
|
|
04565dad00 | ||
|
|
ac0ae06463 | ||
|
|
a8775f757f | ||
|
|
e4f4ed923d | ||
|
|
c7915fa95d | ||
|
|
652b118634 | ||
|
|
f122ff1471 | ||
|
|
f76b90fab9 | ||
|
|
ccd8beae92 | ||
|
|
33f01009d2 | ||
|
|
a0e3a71109 | ||
|
|
f35ecba801 | ||
|
|
0d7f26bd14 | ||
|
|
2f7bdd2640 | ||
|
|
3125e4ddd2 | ||
|
|
c624cb27da | ||
|
|
06adf9a094 | ||
|
|
94ed55ee35 | ||
|
|
95c8603b69 | ||
|
|
9ad2a8272e | ||
|
|
6dc17da00a | ||
|
|
97aa1c6b2c | ||
|
|
084fed8377 | ||
|
|
68d86e4fa1 | ||
|
|
5203373047 | ||
|
|
86de481ba1 | ||
|
|
5d075a97dd | ||
|
|
e89499f5db | ||
|
|
a1f0208767 | ||
|
|
d1964c20ab | ||
|
|
1e035bf2e5 | ||
|
|
f3b87f9cf9 | ||
|
|
d2ea45e1eb | ||
|
|
58ad6b1a40 | ||
|
|
772ef3cd10 | ||
|
|
204dfd52c8 | ||
|
|
244d3452d0 | ||
|
|
e65f8a7150 | ||
|
|
90d6991b9d | ||
|
|
87ca3f203a | ||
|
|
3f3a20e178 | ||
|
|
ce1a19fdbe | ||
|
|
26ac92a6b1 | ||
|
|
1c7dd0a533 | ||
|
|
ceb29d76b2 | ||
|
|
6efb354cce | ||
|
|
8ae242a6ed | ||
|
|
5f0f92fd15 | ||
|
|
e521fa3554 | ||
|
|
b8f73cf21f | ||
|
|
d068cddbaf | ||
|
|
99422835ab | ||
|
|
68263360c7 | ||
|
|
34f5ddab0f | ||
|
|
0cc4630fc6 | ||
|
|
ee57c79784 | ||
|
|
7e41b8df27 | ||
|
|
79c1d7bafa | ||
|
|
652b2fee86 | ||
|
|
dfb871c26a | ||
|
|
99f74acb94 | ||
|
|
83f7682606 | ||
|
|
c4a07f3098 | ||
|
|
c9451f4f35 | ||
|
|
dff4922783 | ||
|
|
c05b666f3c | ||
|
|
9905024f08 | ||
|
|
b0cb457287 | ||
|
|
d4946c1c08 | ||
|
|
1d745b5cbd | ||
|
|
c99785f266 | ||
|
|
9ecdec3f45 | ||
|
|
866f45bd95 | ||
|
|
320a79dbff | ||
|
|
1e0b6f1aad | ||
|
|
251ad6727a | ||
|
|
1b1f56d183 | ||
|
|
7dc05a45ac | ||
|
|
7257bab289 | ||
|
|
48a76c2d0d | ||
|
|
d78c83b236 | ||
|
|
d9b0415047 | ||
|
|
af2e3de96d | ||
|
|
1c142cb170 | ||
|
|
d5dae7d444 | ||
|
|
85816be967 | ||
|
|
e66d658955 | ||
|
|
1916fded95 | ||
|
|
8cc8f4c49c | ||
|
|
1a3bf16729 | ||
|
|
5304b533fc | ||
|
|
0156d47d97 | ||
|
|
b04ac6cdba | ||
|
|
f43e4200da | ||
|
|
2ba7aef2b1 | ||
|
|
7a1932cab4 | ||
|
|
59f189229c | ||
|
|
29af7a1d29 | ||
|
|
3d9ddc53e6 | ||
|
|
1a34feda65 | ||
|
|
d8c1081578 | ||
|
|
1085a1da62 | ||
|
|
576db8e0e1 | ||
|
|
fe9dc4a28d | ||
|
|
d2aaae2340 | ||
|
|
84e40cbc2c | ||
|
|
42672a9ae2 | ||
|
|
2dd2173553 | ||
|
|
fcd5400cda | ||
|
|
23b69bb7a7 | ||
|
|
c31b0945d8 | ||
|
|
c71a4ac4e8 | ||
|
|
38c7ca64cb | ||
|
|
bc187fe7b3 | ||
|
|
548c0db0ae | ||
|
|
3ecd1307b0 | ||
|
|
b51661b790 | ||
|
|
02d6e41ce3 | ||
|
|
f884aed25e | ||
|
|
f2dee1a470 | ||
|
|
d5d806760f | ||
|
|
efe2a8f789 | ||
|
|
69bbed999f | ||
|
|
5b7d4184f0 | ||
|
|
422b7ed5ef | ||
|
|
66a2b916f2 | ||
|
|
27ac253098 | ||
|
|
1839b24b90 | ||
|
|
a60ebb6210 | ||
|
|
4dba4f3c02 | ||
|
|
6727af2f35 | ||
|
|
5bcf334bc5 | ||
|
|
ef49bd685e | ||
|
|
092562fb94 | ||
|
|
44e2983803 | ||
|
|
2e848da1a8 | ||
|
|
22ae4ecb23 | ||
|
|
1642ac2394 | ||
|
|
be8aa93cca | ||
|
|
fc7ba85639 | ||
|
|
11d1dd6419 | ||
|
|
e8bd7270e9 | ||
|
|
99e2afa81b | ||
|
|
676792d07d | ||
|
|
0bec2bcd1e | ||
|
|
25328048aa | ||
|
|
e51d2137cf | ||
|
|
5695677873 | ||
|
|
9d86f582ac | ||
|
|
97b0558f71 | ||
|
|
e825be1c79 | ||
|
|
88252c3dfc | ||
|
|
02997b593f | ||
|
|
9de81a0b14 | ||
|
|
ab13182091 | ||
|
|
e75e4580ae | ||
|
|
b7f031d199 | ||
|
|
808d27be2d | ||
|
|
dcf8751ff7 | ||
|
|
ec6e3d80ee | ||
|
|
5a9badf683 | ||
|
|
dd11dcef93 | ||
|
|
e9851957f0 | ||
|
|
57cfbb3770 | ||
|
|
da2590819d | ||
|
|
d74d93a522 | ||
|
|
b3eadcc990 | ||
|
|
f4d4405c20 | ||
|
|
1ebec0e2e5 | ||
|
|
4433347ff3 | ||
|
|
08c539ba51 | ||
|
|
4e538dd138 | ||
|
|
66373ccab2 | ||
|
|
9c82c0700d | ||
|
|
a480e50c16 | ||
|
|
4391bf4f70 | ||
|
|
18f46cb527 | ||
|
|
32b89adc04 | ||
|
|
5a213fcad9 | ||
|
|
d6f1d1bb3c | ||
|
|
c8c0d0e804 | ||
|
|
aeddf2a860 | ||
|
|
b876a5f7bd | ||
|
|
af8949c291 | ||
|
|
e3da38df7c | ||
|
|
a16079e38d | ||
|
|
e13b0fac63 | ||
|
|
26190350c4 | ||
|
|
7598cc62d3 | ||
|
|
b3211fa8c2 | ||
|
|
8dcc909b6e | ||
|
|
fafaf2c490 | ||
|
|
11e2f19503 | ||
|
|
d67bd0eff2 | ||
|
|
43bbea9a7c | ||
|
|
dc366c53a7 | ||
|
|
fe1ede26be | ||
|
|
aca799b28c | ||
|
|
1017a0af1d | ||
|
|
ff25484a34 | ||
|
|
af922920ab | ||
|
|
919a3f1b91 | ||
|
|
1ec8e63e9e | ||
|
|
96c76a059a | ||
|
|
608701aea7 | ||
|
|
142d44a01f | ||
|
|
83c0f537ce | ||
|
|
aeab25b7fe | ||
|
|
ef255c6a5c | ||
|
|
474ebed0ee | ||
|
|
6e55604dc3 | ||
|
|
2dfdfefb1a | ||
|
|
48f78cf80a | ||
|
|
69cc28dd4b | ||
|
|
adb8bcf791 | ||
|
|
bd22d30674 | ||
|
|
afbf1cc33f | ||
|
|
485df59c5b | ||
|
|
43308b326d | ||
|
|
6c9708c700 | ||
|
|
4a962aa75a | ||
|
|
e786158071 | ||
|
|
ea5a767948 | ||
|
|
bde65a6c57 | ||
|
|
94c0a436b5 | ||
|
|
bddf1adeba | ||
|
|
9406a577c2 | ||
|
|
40344726e1 | ||
|
|
08ffea23b7 | ||
|
|
f1ddaa8051 | ||
|
|
7ae62c1060 | ||
|
|
7c51046d8a | ||
|
|
df6d75e2c5 | ||
|
|
3c88d9666d | ||
|
|
ee74025f38 | ||
|
|
d49c7d47a2 | ||
|
|
8f18e9bbbc | ||
|
|
be96a3785b | ||
|
|
f3aeb5ec9f | ||
|
|
71ea61e7a6 | ||
|
|
c6c73e5e59 | ||
|
|
ae242f8480 | ||
|
|
351a17f68d | ||
|
|
1c4f7cb339 | ||
|
|
2713af189f | ||
|
|
be62d1a915 | ||
|
|
68d71a6767 | ||
|
|
46c88ad1bf | ||
|
|
917151123f | ||
|
|
b9e9d6a787 | ||
|
|
db20077c37 | ||
|
|
ec3d027f90 | ||
|
|
50678085c9 | ||
|
|
5af4785ba1 | ||
|
|
f59d42d5d3 | ||
|
|
fc7ae1ee96 | ||
|
|
2699bb9a44 | ||
|
|
212f5b4082 | ||
|
|
9591f48077 | ||
|
|
0a8494a0f2 | ||
|
|
39f2a4ab71 | ||
|
|
39aa09ca36 | ||
|
|
45c5a2ae78 | ||
|
|
a1c612f833 | ||
|
|
bc39bb32d4 | ||
|
|
884308ecb5 | ||
|
|
2d7f232c3d | ||
|
|
afa65d9ead | ||
|
|
605c88a029 | ||
|
|
09994d1730 | ||
|
|
bb9c99ebf6 | ||
|
|
d4819886a6 | ||
|
|
23445123a2 | ||
|
|
773bb43b41 | ||
|
|
493dddde02 | ||
|
|
2e5704c6b4 | ||
|
|
be6c843bba | ||
|
|
064d677fdc | ||
|
|
084ab782da | ||
|
|
1a10a1ef75 | ||
|
|
acc77e4e6c | ||
|
|
2c0ff9be60 | ||
|
|
b9f5946cf0 | ||
|
|
c403968fa8 | ||
|
|
14baef425e | ||
|
|
8da532da93 | ||
|
|
facf701897 | ||
|
|
f9fe9ea5d6 | ||
|
|
f5a80c3007 | ||
|
|
7cff37e707 | ||
|
|
4e8c6b3fc4 | ||
|
|
82282c1c75 | ||
|
|
d7379f29da | ||
|
|
000159226d | ||
|
|
4ebdb8d24b | ||
|
|
fc2df08e15 | ||
|
|
aaa83dc19a | ||
|
|
adff0d5da0 | ||
|
|
285d6931bc | ||
|
|
114b687c5c | ||
|
|
a26ade3988 | ||
|
|
94e2983310 | ||
|
|
fb2f5f6756 | ||
|
|
d7b818cba2 | ||
|
|
8d885d14ed | ||
|
|
39e0023a5f | ||
|
|
84faac0cd6 | ||
|
|
1bce639d35 | ||
|
|
f2061610b9 | ||
|
|
8716f9b41a | ||
|
|
a24c817bb7 | ||
|
|
47997e0056 | ||
|
|
70fd7bea99 | ||
|
|
52565375b3 | ||
|
|
2c327d9eee | ||
|
|
f5b4a0788e | ||
|
|
1aa338fe3e | ||
|
|
5615d8d870 | ||
|
|
dddc5181e7 | ||
|
|
3b78fa2d5d | ||
|
|
2a64f5e3ce | ||
|
|
8f5b53e3ce | ||
|
|
cd84e78b27 |
No files matched your search
@@ -0,0 +1,20 @@
|
||||
* text=auto
|
||||
|
||||
*.html text eol=lf
|
||||
*.java text eol=lf
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
*.jsp text eol=lf
|
||||
*.md text eol=lf
|
||||
*.properties text eol=lf
|
||||
*.svg text auto
|
||||
*.xml text eol=lf
|
||||
*.xsl text eol=lf
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.gif binary
|
||||
*.ttf binary
|
||||
*.eot binary
|
||||
*.otf binary
|
||||
*.woff binary
|
||||
+16
-5
@@ -1,10 +1,21 @@
|
||||
language: java
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.m2
|
||||
env:
|
||||
global:
|
||||
- MAVEN_SKIP_RC=true
|
||||
- MAVEN_OPTS="-Xms512m -Xmx2048m"
|
||||
|
||||
before_cache:
|
||||
- rm -rf $HOME/.m2/repository/org/keycloak
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
|
||||
before_script:
|
||||
- export MAVEN_SKIP_RC=true
|
||||
|
||||
install:
|
||||
- mvn install -Pdistribution -DskipTests=true -B -V -q
|
||||
|
||||
script:
|
||||
- mvn test -B
|
||||
- mvn -file testsuite/integration-arquillian test -B -Pno-console
|
||||
|
||||
sudo: false
|
||||
@@ -7,7 +7,7 @@ Keycloak is an SSO Service for web apps and REST services. For more information
|
||||
Building
|
||||
--------
|
||||
|
||||
Ensure you have JDK 7 (or newer), Maven 3.2.1 (or newer) and Git installed
|
||||
Ensure you have JDK 8 (or newer), Maven 3.2.1 (or newer) and Git installed
|
||||
|
||||
java -version
|
||||
mvn -version
|
||||
|
||||
+50
-50
@@ -1,50 +1,50 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-core</artifactId>
|
||||
<name>Keycloak Broker Core</name>
|
||||
<description/>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-events-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jaxrs</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>log4j</groupId>
|
||||
<artifactId>log4j</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.8.0.CR3</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-core</artifactId>
|
||||
<name>Keycloak Broker Core</name>
|
||||
<description/>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-events-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jaxrs</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>log4j</groupId>
|
||||
<artifactId>log4j</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -58,6 +58,11 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel>
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response performLogin(AuthenticationRequest request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
return null;
|
||||
@@ -87,4 +92,9 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel>
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderDataMarshaller getMarshaller() {
|
||||
return new DefaultDataMarshaller();
|
||||
}
|
||||
}
|
||||
+43
-38
@@ -1,38 +1,43 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.broker.provider.IdentityProviderMapper;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public abstract class AbstractIdentityProviderMapper implements IdentityProviderMapper {
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderMapper create(KeycloakSession session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(org.keycloak.Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
}
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public abstract class AbstractIdentityProviderMapper implements IdentityProviderMapper {
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderMapper create(KeycloakSession session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(org.keycloak.Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,14 @@
|
||||
*/
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -43,6 +48,7 @@ public class BrokeredIdentityContext {
|
||||
private IdentityProviderModel idpConfig;
|
||||
private IdentityProvider idp;
|
||||
private Map<String, Object> contextData = new HashMap<>();
|
||||
private ClientSessionModel clientSession;
|
||||
|
||||
public BrokeredIdentityContext(String id) {
|
||||
if (id == null) {
|
||||
@@ -150,6 +156,22 @@ public class BrokeredIdentityContext {
|
||||
this.contextData = contextData;
|
||||
}
|
||||
|
||||
// Set the attribute, which will be available on "Update profile" page and in authenticators
|
||||
public void setUserAttribute(String attributeName, String attributeValue) {
|
||||
List<String> list = new ArrayList<>();
|
||||
list.add(attributeValue);
|
||||
getContextData().put(Constants.USER_ATTRIBUTES_PREFIX + attributeName, list);
|
||||
}
|
||||
|
||||
public String getUserAttribute(String attributeName) {
|
||||
List<String> userAttribute = (List<String>) getContextData().get(Constants.USER_ATTRIBUTES_PREFIX + attributeName);
|
||||
if (userAttribute == null || userAttribute.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
return userAttribute.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
@@ -166,6 +188,14 @@ public class BrokeredIdentityContext {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public ClientSessionModel getClientSession() {
|
||||
return clientSession;
|
||||
}
|
||||
|
||||
public void setClientSession(ClientSessionModel clientSession) {
|
||||
this.clientSession = clientSession;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
if (name != null) {
|
||||
int i = name.lastIndexOf(' ');
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class DefaultDataMarshaller implements IdentityProviderDataMarshaller {
|
||||
|
||||
@Override
|
||||
public String serialize(Object value) {
|
||||
if (value instanceof String) {
|
||||
return (String) value;
|
||||
} else {
|
||||
try {
|
||||
byte[] bytes = JsonSerialization.writeValueAsBytes(value);
|
||||
return Base64Url.encode(bytes);
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T deserialize(String serialized, Class<T> clazz) {
|
||||
try {
|
||||
if (clazz.equals(String.class)) {
|
||||
return clazz.cast(serialized);
|
||||
} else {
|
||||
byte[] bytes = Base64Url.decode(serialized);
|
||||
if (List.class.isAssignableFrom(clazz)) {
|
||||
List list = JsonSerialization.readValue(bytes, List.class);
|
||||
return clazz.cast(list);
|
||||
} else {
|
||||
return JsonSerialization.readValue(bytes, clazz);
|
||||
}
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class HardcodedAttributeMapper extends AbstractIdentityProviderMapper {
|
||||
public static final String ATTRIBUTE = "attribute";
|
||||
public static final String ATTRIBUTE_VALUE = "attribute.value";
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE);
|
||||
property.setLabel("User Attribute");
|
||||
property.setHelpText("Name of user attribute you want to hardcode");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_VALUE);
|
||||
property.setLabel("User Attribute Value");
|
||||
property.setHelpText("Value you want to hardcode");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Hardcoded Attribute";
|
||||
}
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {ANY_PROVIDER};
|
||||
|
||||
|
||||
public static final String PROVIDER_ID = "hardcoded-attribute-idp-mapper";
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(ATTRIBUTE);
|
||||
String attributeValue = mapperModel.getConfig().get(ATTRIBUTE_VALUE);
|
||||
context.setUserAttribute(attribute, attributeValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(ATTRIBUTE);
|
||||
String attributeValue = mapperModel.getConfig().get(ATTRIBUTE_VALUE);
|
||||
user.setSingleAttribute(attribute, attributeValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "When user is imported from provider, hardcode a value to a specific user attribute.";
|
||||
}
|
||||
}
|
||||
@@ -1,110 +1,79 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class HardcodedRoleMapper extends AbstractIdentityProviderMapper {
|
||||
public static final String ROLE = "role";
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ROLE);
|
||||
property.setLabel("Role");
|
||||
property.setHelpText("Role to grant to user. To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static String[] parseRole(String role) {
|
||||
int scopeIndex = role.indexOf('.');
|
||||
if (scopeIndex > -1) {
|
||||
String appName = role.substring(0, scopeIndex);
|
||||
role = role.substring(scopeIndex + 1);
|
||||
String[] rtn = {appName, role};
|
||||
return rtn;
|
||||
} else {
|
||||
String[] rtn = {null, role};
|
||||
return rtn;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static RoleModel getRoleFromString(RealmModel realm, String roleName) {
|
||||
String[] parsedRole = parseRole(roleName);
|
||||
RoleModel role = null;
|
||||
if (parsedRole[0] == null) {
|
||||
role = realm.getRole(parsedRole[1]);
|
||||
} else {
|
||||
ClientModel client = realm.getClientByClientId(parsedRole[0]);
|
||||
role = client.getRole(parsedRole[1]);
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Role Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Hardcoded Role";
|
||||
}
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {ANY_PROVIDER};
|
||||
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-hardcoded-role-idp-mapper";
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(ROLE);
|
||||
RoleModel role = getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.grantRole(role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "When user is imported from provider, hardcode a role mapping for it.";
|
||||
}
|
||||
}
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class HardcodedRoleMapper extends AbstractIdentityProviderMapper {
|
||||
public static final String ROLE = "role";
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ROLE);
|
||||
property.setLabel("Role");
|
||||
property.setHelpText("Role to grant to user. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
|
||||
property.setType(ProviderConfigProperty.ROLE_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Role Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Hardcoded Role";
|
||||
}
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {ANY_PROVIDER};
|
||||
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-hardcoded-role-idp-mapper";
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(ROLE);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.grantRole(role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "When user is imported from provider, hardcode a role mapping for it.";
|
||||
}
|
||||
}
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class HardcodedUserSessionAttributeMapper extends AbstractIdentityProviderMapper {
|
||||
public static final String ATTRIBUTE = "attribute";
|
||||
public static final String ATTRIBUTE_VALUE = "attribute.value";
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE);
|
||||
property.setLabel("User Session Attribute");
|
||||
property.setHelpText("Name of user session attribute you want to hardcode");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_VALUE);
|
||||
property.setLabel("User Session Attribute Value");
|
||||
property.setHelpText("Value you want to hardcode");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Hardcoded User Session Attribute";
|
||||
}
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {ANY_PROVIDER};
|
||||
|
||||
|
||||
public static final String PROVIDER_ID = "hardcoded-user-session-attribute-idp-mapper";
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(ATTRIBUTE);
|
||||
String attributeValue = mapperModel.getConfig().get(ATTRIBUTE_VALUE);
|
||||
context.getClientSession().setUserSessionNote(attribute, attributeValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(ATTRIBUTE);
|
||||
String attributeValue = mapperModel.getConfig().get(ATTRIBUTE_VALUE);
|
||||
context.getClientSession().setUserSessionNote(attribute, attributeValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "When user is imported from provider, hardcode a value to a specific user session attribute.";
|
||||
}
|
||||
}
|
||||
@@ -103,4 +103,10 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
|
||||
*/
|
||||
Response export(UriInfo uriInfo, RealmModel realm, String format);
|
||||
|
||||
/**
|
||||
* Implementation of marshaller to serialize/deserialize attached data to Strings, which can be saved in clientSession
|
||||
* @return
|
||||
*/
|
||||
IdentityProviderDataMarshaller getMarshaller();
|
||||
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface IdentityProviderDataMarshaller {
|
||||
|
||||
String serialize(Object obj);
|
||||
<T> T deserialize(String serialized, Class<T> clazz);
|
||||
|
||||
}
|
||||
+58
-56
@@ -1,56 +1,58 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ConfiguredProvider;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface IdentityProviderMapper extends Provider, ProviderFactory<IdentityProviderMapper>,ConfiguredProvider {
|
||||
public static final String ANY_PROVIDER = "*";
|
||||
|
||||
String[] getCompatibleProviders();
|
||||
String getDisplayCategory();
|
||||
String getDisplayType();
|
||||
|
||||
/**
|
||||
* Called to determine what keycloak username and email to use to process the login request from the external IDP
|
||||
* Usually used to map BrokeredIdentityContet.username or email.
|
||||
*
|
||||
* @param session
|
||||
* @param realm
|
||||
* @param mapperModel
|
||||
* @param context
|
||||
*/
|
||||
void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context);
|
||||
|
||||
/**
|
||||
* Called after UserModel is created for first time for this user.
|
||||
*
|
||||
* @param session
|
||||
* @param realm
|
||||
* @param user
|
||||
* @param mapperModel
|
||||
* @param context
|
||||
*/
|
||||
void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context);
|
||||
|
||||
/**
|
||||
* Called when this user has logged in before and has already been imported.
|
||||
*
|
||||
* @param session
|
||||
* @param realm
|
||||
* @param user
|
||||
* @param mapperModel
|
||||
* @param context
|
||||
*/
|
||||
void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context);
|
||||
|
||||
|
||||
}
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ConfiguredProvider;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface IdentityProviderMapper extends Provider, ProviderFactory<IdentityProviderMapper>,ConfiguredProvider {
|
||||
public static final String ANY_PROVIDER = "*";
|
||||
|
||||
String[] getCompatibleProviders();
|
||||
String getDisplayCategory();
|
||||
String getDisplayType();
|
||||
|
||||
/**
|
||||
* Called to determine what keycloak username and email to use to process the login request from the external IDP.
|
||||
* It's called before "FirstBrokerLogin" flow, so can be used to map attributes to BrokeredIdentityContext ( BrokeredIdentityContext.setUserAttribute ),
|
||||
* which will be available on "Review Profile" page and in authenticators during FirstBrokerLogin flow
|
||||
*
|
||||
*
|
||||
* @param session
|
||||
* @param realm
|
||||
* @param mapperModel
|
||||
* @param context
|
||||
*/
|
||||
void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context);
|
||||
|
||||
/**
|
||||
* Called after UserModel is created for first time for this user. Called after "FirstBrokerLogin" flow
|
||||
*
|
||||
* @param session
|
||||
* @param realm
|
||||
* @param user
|
||||
* @param mapperModel
|
||||
* @param context
|
||||
*/
|
||||
void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context);
|
||||
|
||||
/**
|
||||
* Called when this user has logged in before and has already been imported.
|
||||
*
|
||||
* @param session
|
||||
* @param realm
|
||||
* @param user
|
||||
* @param mapperModel
|
||||
* @param context
|
||||
*/
|
||||
void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context);
|
||||
|
||||
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.keycloak.broker.provider.util;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
@@ -24,6 +27,9 @@ public class SimpleHttp {
|
||||
private Map<String, String> headers;
|
||||
private Map<String, String> params;
|
||||
|
||||
private SSLSocketFactory sslFactory;
|
||||
private HostnameVerifier hostnameVerifier;
|
||||
|
||||
protected SimpleHttp(String url, String method) {
|
||||
this.url = url;
|
||||
this.method = method;
|
||||
@@ -53,6 +59,15 @@ public class SimpleHttp {
|
||||
return this;
|
||||
}
|
||||
|
||||
public SimpleHttp sslFactory(SSLSocketFactory factory) {
|
||||
sslFactory = factory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SimpleHttp hostnameVerifier(HostnameVerifier verifier) {
|
||||
hostnameVerifier = verifier;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String asString() throws IOException {
|
||||
boolean get = method.equals("GET");
|
||||
@@ -85,6 +100,7 @@ public class SimpleHttp {
|
||||
}
|
||||
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
setupTruststoreIfApplicable(connection);
|
||||
OutputStream os = null;
|
||||
InputStream is = null;
|
||||
|
||||
@@ -171,6 +187,7 @@ public class SimpleHttp {
|
||||
}
|
||||
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
setupTruststoreIfApplicable(connection);
|
||||
OutputStream os = null;
|
||||
InputStream is = null;
|
||||
|
||||
@@ -235,4 +252,13 @@ public class SimpleHttp {
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
private void setupTruststoreIfApplicable(HttpURLConnection connection) {
|
||||
if (connection instanceof HttpsURLConnection && sslFactory != null) {
|
||||
HttpsURLConnection con = (HttpsURLConnection) connection;
|
||||
con.setSSLSocketFactory(sslFactory);
|
||||
if (hostnameVerifier != null) {
|
||||
con.setHostnameVerifier(hostnameVerifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -1 +1,3 @@
|
||||
org.keycloak.broker.provider.HardcodedRoleMapper
|
||||
org.keycloak.broker.provider.HardcodedRoleMapper
|
||||
org.keycloak.broker.provider.HardcodedAttributeMapper
|
||||
org.keycloak.broker.provider.HardcodedUserSessionAttributeMapper
|
||||
@@ -1,2 +1,2 @@
|
||||
org.keycloak.broker.provider.IdentityProviderSpi
|
||||
org.keycloak.broker.provider.IdentityProviderSpi
|
||||
org.keycloak.broker.provider.IdentityProviderMapperSpi
|
||||
+58
-58
@@ -1,58 +1,58 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-oidc</artifactId>
|
||||
<name>Keycloak Broker - OpenID Connect Identity Provider</name>
|
||||
<description/>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-broker-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.jackson</groupId>
|
||||
<artifactId>jackson-core-asl</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.jackson</groupId>
|
||||
<artifactId>jackson-mapper-asl</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-events-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-services</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.8.0.CR3</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-oidc</artifactId>
|
||||
<name>Keycloak Broker - OpenID Connect Identity Provider</name>
|
||||
<description/>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-broker-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.jackson</groupId>
|
||||
<artifactId>jackson-core-asl</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.jackson</groupId>
|
||||
<artifactId>jackson-mapper-asl</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-events-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-services</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
+6
-2
@@ -20,13 +20,14 @@ package org.keycloak.broker.oidc;
|
||||
import org.codehaus.jackson.JsonNode;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.broker.provider.AbstractIdentityProvider;
|
||||
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.connections.truststore.JSSETruststoreConfigurator;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
@@ -247,12 +248,15 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||
}
|
||||
|
||||
public SimpleHttp generateTokenRequest(String authorizationCode) {
|
||||
JSSETruststoreConfigurator configurator = new JSSETruststoreConfigurator(session);
|
||||
return SimpleHttp.doPost(getConfig().getTokenUrl())
|
||||
.param(OAUTH2_PARAMETER_CODE, authorizationCode)
|
||||
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
|
||||
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret())
|
||||
.param(OAUTH2_PARAMETER_REDIRECT_URI, uriInfo.getAbsolutePath().toString())
|
||||
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE);
|
||||
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE)
|
||||
.sslFactory(configurator.getSSLSocketFactory())
|
||||
.hostnameVerifier(configurator.getHostnameVerifier());
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
-114
@@ -1,114 +1,121 @@
|
||||
package org.keycloak.broker.oidc;
|
||||
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.constants.AdapterConstants;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.adapters.action.AdminAction;
|
||||
import org.keycloak.representations.adapters.action.LogoutAction;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.security.PublicKey;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
|
||||
|
||||
public static final String VALIDATED_ACCESS_TOKEN = "VALIDATED_ACCESS_TOKEN";
|
||||
|
||||
public KeycloakOIDCIdentityProvider(OIDCIdentityProviderConfig config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) {
|
||||
return new KeycloakEndpoint(callback, realm, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processAccessTokenResponse(BrokeredIdentityContext context, PublicKey idpKey, AccessTokenResponse response) {
|
||||
JsonWebToken access = validateToken(idpKey, response.getToken());
|
||||
context.getContextData().put(VALIDATED_ACCESS_TOKEN, access);
|
||||
}
|
||||
|
||||
protected class KeycloakEndpoint extends OIDCEndpoint {
|
||||
public KeycloakEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
|
||||
super(callback, realm, event);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path(AdapterConstants.K_LOGOUT)
|
||||
public Response backchannelLogout(String input) {
|
||||
JWSInput token = new JWSInput(input);
|
||||
PublicKey key = getExternalIdpKey();
|
||||
if (key != null) {
|
||||
if (!verify(token, key)) {
|
||||
logger.warn("Failed to verify logout request");
|
||||
return Response.status(400).build();
|
||||
}
|
||||
}
|
||||
LogoutAction action = null;
|
||||
try {
|
||||
action = JsonSerialization.readValue(token.getContent(), LogoutAction.class);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (!validateAction(action)) return Response.status(400).build();
|
||||
if (action.getKeycloakSessionIds() != null) {
|
||||
for (String sessionId : action.getKeycloakSessionIds()) {
|
||||
String brokerSessionId = getConfig().getAlias() + "." + sessionId;
|
||||
UserSessionModel userSession = session.sessions().getUserSessionByBrokerSessionId(realm, brokerSessionId);
|
||||
if (userSession != null
|
||||
&& userSession.getState() != UserSessionModel.State.LOGGING_OUT
|
||||
&& userSession.getState() != UserSessionModel.State.LOGGED_OUT
|
||||
) {
|
||||
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
protected boolean validateAction(AdminAction action) {
|
||||
if (!action.validate()) {
|
||||
logger.warn("admin request failed, not validated" + action.getAction());
|
||||
return false;
|
||||
}
|
||||
if (action.isExpired()) {
|
||||
logger.warn("admin request failed, expired token");
|
||||
return false;
|
||||
}
|
||||
if (!getConfig().getClientId().equals(action.getResource())) {
|
||||
logger.warn("Resource name does not match");
|
||||
return false;
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SimpleHttp generateTokenRequest(String authorizationCode) {
|
||||
return super.generateTokenRequest(authorizationCode)
|
||||
.param(AdapterConstants.CLIENT_SESSION_STATE, "n/a"); // hack to get backchannel logout to work
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
package org.keycloak.broker.oidc;
|
||||
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.constants.AdapterConstants;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.adapters.action.AdminAction;
|
||||
import org.keycloak.representations.adapters.action.LogoutAction;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.security.PublicKey;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
|
||||
|
||||
public static final String VALIDATED_ACCESS_TOKEN = "VALIDATED_ACCESS_TOKEN";
|
||||
|
||||
public KeycloakOIDCIdentityProvider(OIDCIdentityProviderConfig config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) {
|
||||
return new KeycloakEndpoint(callback, realm, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processAccessTokenResponse(BrokeredIdentityContext context, PublicKey idpKey, AccessTokenResponse response) {
|
||||
JsonWebToken access = validateToken(idpKey, response.getToken());
|
||||
context.getContextData().put(VALIDATED_ACCESS_TOKEN, access);
|
||||
}
|
||||
|
||||
protected class KeycloakEndpoint extends OIDCEndpoint {
|
||||
public KeycloakEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
|
||||
super(callback, realm, event);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path(AdapterConstants.K_LOGOUT)
|
||||
public Response backchannelLogout(String input) {
|
||||
JWSInput token = null;
|
||||
try {
|
||||
token = new JWSInput(input);
|
||||
} catch (JWSInputException e) {
|
||||
logger.warn("Failed to verify logout request");
|
||||
return Response.status(400).build();
|
||||
}
|
||||
PublicKey key = getExternalIdpKey();
|
||||
if (key != null) {
|
||||
if (!verify(token, key)) {
|
||||
logger.warn("Failed to verify logout request");
|
||||
return Response.status(400).build();
|
||||
}
|
||||
}
|
||||
LogoutAction action = null;
|
||||
try {
|
||||
action = JsonSerialization.readValue(token.getContent(), LogoutAction.class);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (!validateAction(action)) return Response.status(400).build();
|
||||
if (action.getKeycloakSessionIds() != null) {
|
||||
for (String sessionId : action.getKeycloakSessionIds()) {
|
||||
String brokerSessionId = getConfig().getAlias() + "." + sessionId;
|
||||
UserSessionModel userSession = session.sessions().getUserSessionByBrokerSessionId(realm, brokerSessionId);
|
||||
if (userSession != null
|
||||
&& userSession.getState() != UserSessionModel.State.LOGGING_OUT
|
||||
&& userSession.getState() != UserSessionModel.State.LOGGED_OUT
|
||||
) {
|
||||
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
protected boolean validateAction(AdminAction action) {
|
||||
if (!action.validate()) {
|
||||
logger.warn("admin request failed, not validated" + action.getAction());
|
||||
return false;
|
||||
}
|
||||
if (action.isExpired()) {
|
||||
logger.warn("admin request failed, expired token");
|
||||
return false;
|
||||
}
|
||||
if (!getConfig().getClientId().equals(action.getResource())) {
|
||||
logger.warn("Resource name does not match");
|
||||
return false;
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SimpleHttp generateTokenRequest(String authorizationCode) {
|
||||
return super.generateTokenRequest(authorizationCode)
|
||||
.param(AdapterConstants.CLIENT_SESSION_STATE, "n/a"); // hack to get backchannel logout to work
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
@@ -42,7 +43,7 @@ import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.IdentityBrokerService;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.util.PemUtils;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
@@ -282,41 +283,41 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
throw new IdentityBrokerException("No token from server.");
|
||||
}
|
||||
|
||||
JsonWebToken token;
|
||||
try {
|
||||
JWSInput jws = new JWSInput(encodedToken);
|
||||
if (!verify(jws, key)) {
|
||||
throw new IdentityBrokerException("token signature validation failed");
|
||||
}
|
||||
JsonWebToken token = jws.readJsonContent(JsonWebToken.class);
|
||||
|
||||
String aud = token.getAudience();
|
||||
String iss = token.getIssuer();
|
||||
|
||||
if (aud != null && !aud.equals(getConfig().getClientId())) {
|
||||
throw new IdentityBrokerException("Wrong audience from token.");
|
||||
}
|
||||
|
||||
if (!token.isActive()) {
|
||||
throw new IdentityBrokerException("Token is no longer valid");
|
||||
}
|
||||
|
||||
String trustedIssuers = getConfig().getIssuer();
|
||||
|
||||
if (trustedIssuers != null) {
|
||||
String[] issuers = trustedIssuers.split(",");
|
||||
|
||||
for (String trustedIssuer : issuers) {
|
||||
if (iss != null && iss.equals(trustedIssuer.trim())) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IdentityBrokerException("Wrong issuer from token. Got: " + iss + " expected: " + getConfig().getIssuer());
|
||||
}
|
||||
return token;
|
||||
} catch (IOException e) {
|
||||
throw new IdentityBrokerException("Could not decode token.", e);
|
||||
token = jws.readJsonContent(JsonWebToken.class);
|
||||
} catch (JWSInputException e) {
|
||||
throw new IdentityBrokerException("Invalid token", e);
|
||||
}
|
||||
|
||||
String iss = token.getIssuer();
|
||||
|
||||
if (!token.hasAudience(getConfig().getClientId())) {
|
||||
throw new IdentityBrokerException("Wrong audience from token.");
|
||||
}
|
||||
|
||||
if (!token.isActive()) {
|
||||
throw new IdentityBrokerException("Token is no longer valid");
|
||||
}
|
||||
|
||||
String trustedIssuers = getConfig().getIssuer();
|
||||
|
||||
if (trustedIssuers != null) {
|
||||
String[] issuers = trustedIssuers.split(",");
|
||||
|
||||
for (String trustedIssuer : issuers) {
|
||||
if (iss != null && iss.equals(trustedIssuer.trim())) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IdentityBrokerException("Wrong issuer from token. Got: " + iss + " expected: " + getConfig().getIssuer());
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+97
-97
@@ -1,97 +1,97 @@
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper {
|
||||
public static final String CLAIM = "claim";
|
||||
public static final String CLAIM_VALUE = "claim.value";
|
||||
|
||||
public static Object getClaimValue(JsonWebToken token, String claim) {
|
||||
String[] split = claim.split("\\.");
|
||||
Map<String, Object> jsonObject = token.getOtherClaims();
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
if (i == split.length - 1) {
|
||||
return jsonObject.get(split[i]);
|
||||
} else {
|
||||
Object val = jsonObject.get(split[i]);
|
||||
if (!(val instanceof Map)) return null;
|
||||
jsonObject = (Map<String, Object>)val;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Object getClaimValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String claim = mapperModel.getConfig().get(CLAIM);
|
||||
return getClaimValue(context, claim);
|
||||
}
|
||||
|
||||
public static Object getClaimValue(BrokeredIdentityContext context, String claim) {
|
||||
{ // search access token
|
||||
JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN);
|
||||
if (token != null) {
|
||||
Object value = getClaimValue(token, claim);
|
||||
if (value != null) return value;
|
||||
}
|
||||
|
||||
}
|
||||
{ // search ID Token
|
||||
JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ID_TOKEN);
|
||||
if (token != null) {
|
||||
Object value = getClaimValue(token, claim);
|
||||
if (value != null) return value;
|
||||
}
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
protected boolean hasClaimValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
Object value = getClaimValue(mapperModel, context);
|
||||
String desiredValue = mapperModel.getConfig().get(CLAIM_VALUE);
|
||||
return valueEquals(desiredValue, value);
|
||||
}
|
||||
|
||||
public boolean valueEquals(String desiredValue, Object value) {
|
||||
if (value instanceof String) {
|
||||
if (desiredValue.equals(value)) return true;
|
||||
} else if (value instanceof Double) {
|
||||
try {
|
||||
if (Double.valueOf(desiredValue).equals(value)) return true;
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
} else if (value instanceof Integer) {
|
||||
try {
|
||||
if (Integer.valueOf(desiredValue).equals(value)) return true;
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
} else if (value instanceof Boolean) {
|
||||
try {
|
||||
if (Boolean.valueOf(desiredValue).equals(value)) return true;
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
} else if (value instanceof List) {
|
||||
List list = (List)value;
|
||||
for (Object val : list) {
|
||||
return valueEquals(desiredValue, val);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper {
|
||||
public static final String CLAIM = "claim";
|
||||
public static final String CLAIM_VALUE = "claim.value";
|
||||
|
||||
public static Object getClaimValue(JsonWebToken token, String claim) {
|
||||
String[] split = claim.split("\\.");
|
||||
Map<String, Object> jsonObject = token.getOtherClaims();
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
if (i == split.length - 1) {
|
||||
return jsonObject.get(split[i]);
|
||||
} else {
|
||||
Object val = jsonObject.get(split[i]);
|
||||
if (!(val instanceof Map)) return null;
|
||||
jsonObject = (Map<String, Object>)val;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Object getClaimValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String claim = mapperModel.getConfig().get(CLAIM);
|
||||
return getClaimValue(context, claim);
|
||||
}
|
||||
|
||||
public static Object getClaimValue(BrokeredIdentityContext context, String claim) {
|
||||
{ // search access token
|
||||
JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN);
|
||||
if (token != null) {
|
||||
Object value = getClaimValue(token, claim);
|
||||
if (value != null) return value;
|
||||
}
|
||||
|
||||
}
|
||||
{ // search ID Token
|
||||
JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ID_TOKEN);
|
||||
if (token != null) {
|
||||
Object value = getClaimValue(token, claim);
|
||||
if (value != null) return value;
|
||||
}
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
protected boolean hasClaimValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
Object value = getClaimValue(mapperModel, context);
|
||||
String desiredValue = mapperModel.getConfig().get(CLAIM_VALUE);
|
||||
return valueEquals(desiredValue, value);
|
||||
}
|
||||
|
||||
public boolean valueEquals(String desiredValue, Object value) {
|
||||
if (value instanceof String) {
|
||||
if (desiredValue.equals(value)) return true;
|
||||
} else if (value instanceof Double) {
|
||||
try {
|
||||
if (Double.valueOf(desiredValue).equals(value)) return true;
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
} else if (value instanceof Integer) {
|
||||
try {
|
||||
if (Integer.valueOf(desiredValue).equals(value)) return true;
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
} else if (value instanceof Boolean) {
|
||||
try {
|
||||
if (Boolean.valueOf(desiredValue).equals(value)) return true;
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
} else if (value instanceof List) {
|
||||
List list = (List)value;
|
||||
for (Object val : list) {
|
||||
if (valueEquals(desiredValue, val)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+206
-206
@@ -1,206 +1,206 @@
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.codehaus.jackson.JsonNode;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProvider;
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
/**
|
||||
* Abstract class for Social Provider mappers which allow mapping of JSON user profile field into Keycloak user
|
||||
* attribute. Concrete mapper classes with own ID and provider mapping must be implemented for each social provider who
|
||||
* uses {@link JsonNode} user profile.
|
||||
*
|
||||
* @author Vlastimil Elias (velias at redhat dot com)
|
||||
*/
|
||||
public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityProviderMapper {
|
||||
|
||||
|
||||
protected static final Logger logger = Logger.getLogger(AbstractJsonUserAttributeMapper.class);
|
||||
|
||||
protected static final Logger LOGGER_DUMP_USER_PROFILE = Logger.getLogger("org.keycloak.social.user_profile_dump");
|
||||
|
||||
private static final String JSON_PATH_DELIMITER = ".";
|
||||
|
||||
/**
|
||||
* Config param where name of mapping source JSON User Profile field is stored.
|
||||
*/
|
||||
public static final String CONF_JSON_FIELD = "jsonField";
|
||||
/**
|
||||
* Config param where name of mapping target USer attribute is stored.
|
||||
*/
|
||||
public static final String CONF_USER_ATTRIBUTE = "userAttribute";
|
||||
|
||||
/**
|
||||
* Key in {@link BrokeredIdentityContext#getContextData()} where {@link JsonNode} with user profile is stored.
|
||||
*/
|
||||
public static final String CONTEXT_JSON_NODE = OIDCIdentityProvider.USER_INFO;
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
ProviderConfigProperty property1;
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(CONF_JSON_FIELD);
|
||||
property1.setLabel("Social Profile JSON Field Path");
|
||||
property1.setHelpText("Path of field in Social provider User Profile JSON data to get value from. You can use dot notation for nesting and square brackets for array index. Eg. 'contact.address[0].country'.");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(CONF_USER_ATTRIBUTE);
|
||||
property.setLabel("User Attribute Name");
|
||||
property.setHelpText("User attribute name to store information into.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store used profile JsonNode into user context for later use by this mapper. Profile data are dumped into special logger if enabled also to allow investigation of the structure.
|
||||
*
|
||||
* @param user context to store profile data into
|
||||
* @param profile to store into context
|
||||
* @param provider identification of social provider to be used in log dump
|
||||
*
|
||||
* @see #importNewUser(KeycloakSession, RealmModel, UserModel, IdentityProviderMapperModel, BrokeredIdentityContext)
|
||||
* @see BrokeredIdentityContext#getContextData()
|
||||
*/
|
||||
public static void storeUserProfileForMapper(BrokeredIdentityContext user, JsonNode profile, String provider) {
|
||||
user.getContextData().put(AbstractJsonUserAttributeMapper.CONTEXT_JSON_NODE, profile);
|
||||
if (LOGGER_DUMP_USER_PROFILE.isDebugEnabled())
|
||||
LOGGER_DUMP_USER_PROFILE.debug("User Profile JSON Data for provider "+provider+": " + profile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Import user profile information if it exists in Social provider JSON data into the specified user attribute.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(CONF_USER_ATTRIBUTE);
|
||||
if (attribute == null || attribute.trim().isEmpty()) {
|
||||
logger.debug("Attribute is not configured");
|
||||
return;
|
||||
}
|
||||
attribute = attribute.trim();
|
||||
|
||||
String value = getJsonValue(mapperModel, context);
|
||||
if (value != null) {
|
||||
user.setAttribute(attribute, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
// we do not update user profile from social provider
|
||||
}
|
||||
|
||||
protected static String getJsonValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
String jsonField = mapperModel.getConfig().get(CONF_JSON_FIELD);
|
||||
if (jsonField == null || jsonField.trim().isEmpty()) {
|
||||
logger.debug("JSON field path is not configured");
|
||||
return null;
|
||||
}
|
||||
jsonField = jsonField.trim();
|
||||
|
||||
if (jsonField.startsWith(JSON_PATH_DELIMITER) || jsonField.endsWith(JSON_PATH_DELIMITER) || jsonField.startsWith("[")) {
|
||||
logger.debug("JSON field path is invalid " + jsonField);
|
||||
return null;
|
||||
}
|
||||
|
||||
JsonNode profileJsonNode = (JsonNode) context.getContextData().get(CONTEXT_JSON_NODE);
|
||||
|
||||
String value = getJsonValue(profileJsonNode, jsonField);
|
||||
|
||||
if (value == null) {
|
||||
logger.debug("User profile JSON value '" + jsonField + "' is not available.");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
protected static String getJsonValue(JsonNode baseNode, String fieldPath) {
|
||||
logger.debug("Going to process JsonNode path " + fieldPath + " on data " + baseNode);
|
||||
if (baseNode != null) {
|
||||
|
||||
int idx = fieldPath.indexOf(JSON_PATH_DELIMITER);
|
||||
|
||||
String currentFieldName = fieldPath;
|
||||
if (idx > 0) {
|
||||
currentFieldName = fieldPath.substring(0, idx).trim();
|
||||
if (currentFieldName.isEmpty()) {
|
||||
logger.debug("JSON path is invalid " + fieldPath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String currentNodeName = currentFieldName;
|
||||
int arrayIndex = -1;
|
||||
if (currentFieldName.endsWith("]")) {
|
||||
int bi = currentFieldName.indexOf("[");
|
||||
if (bi == -1) {
|
||||
logger.debug("Invalid array index construct in " + currentFieldName);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String is = currentFieldName.substring(bi+1, currentFieldName.length() - 1).trim();
|
||||
arrayIndex = Integer.parseInt(is);
|
||||
} catch (Exception e) {
|
||||
logger.debug("Invalid array index construct in " + currentFieldName);
|
||||
return null;
|
||||
}
|
||||
currentNodeName = currentFieldName.substring(0,bi).trim();
|
||||
}
|
||||
|
||||
JsonNode currentNode = baseNode.get(currentNodeName);
|
||||
if (arrayIndex > -1 && currentNode.isArray()) {
|
||||
logger.debug("Going to take array node at index " + arrayIndex);
|
||||
currentNode = currentNode.get(arrayIndex);
|
||||
}
|
||||
|
||||
if (currentNode == null) {
|
||||
logger.debug("JsonNode not found for name " + currentFieldName);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (idx < 0) {
|
||||
if (!currentNode.isValueNode()) {
|
||||
logger.debug("JsonNode is not value node for name " + currentFieldName);
|
||||
return null;
|
||||
}
|
||||
String ret = currentNode.asText();
|
||||
if (ret != null && !ret.trim().isEmpty())
|
||||
return ret.trim();
|
||||
} else {
|
||||
return getJsonValue(currentNode, fieldPath.substring(idx + 1));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.codehaus.jackson.JsonNode;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProvider;
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
/**
|
||||
* Abstract class for Social Provider mappers which allow mapping of JSON user profile field into Keycloak user
|
||||
* attribute. Concrete mapper classes with own ID and provider mapping must be implemented for each social provider who
|
||||
* uses {@link JsonNode} user profile.
|
||||
*
|
||||
* @author Vlastimil Elias (velias at redhat dot com)
|
||||
*/
|
||||
public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityProviderMapper {
|
||||
|
||||
|
||||
protected static final Logger logger = Logger.getLogger(AbstractJsonUserAttributeMapper.class);
|
||||
|
||||
protected static final Logger LOGGER_DUMP_USER_PROFILE = Logger.getLogger("org.keycloak.social.user_profile_dump");
|
||||
|
||||
private static final String JSON_PATH_DELIMITER = ".";
|
||||
|
||||
/**
|
||||
* Config param where name of mapping source JSON User Profile field is stored.
|
||||
*/
|
||||
public static final String CONF_JSON_FIELD = "jsonField";
|
||||
/**
|
||||
* Config param where name of mapping target USer attribute is stored.
|
||||
*/
|
||||
public static final String CONF_USER_ATTRIBUTE = "userAttribute";
|
||||
|
||||
/**
|
||||
* Key in {@link BrokeredIdentityContext#getContextData()} where {@link JsonNode} with user profile is stored.
|
||||
*/
|
||||
public static final String CONTEXT_JSON_NODE = OIDCIdentityProvider.USER_INFO;
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
ProviderConfigProperty property1;
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(CONF_JSON_FIELD);
|
||||
property1.setLabel("Social Profile JSON Field Path");
|
||||
property1.setHelpText("Path of field in Social provider User Profile JSON data to get value from. You can use dot notation for nesting and square brackets for array index. Eg. 'contact.address[0].country'.");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(CONF_USER_ATTRIBUTE);
|
||||
property.setLabel("User Attribute Name");
|
||||
property.setHelpText("User attribute name to store information into.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store used profile JsonNode into user context for later use by this mapper. Profile data are dumped into special logger if enabled also to allow investigation of the structure.
|
||||
*
|
||||
* @param user context to store profile data into
|
||||
* @param profile to store into context
|
||||
* @param provider identification of social provider to be used in log dump
|
||||
*
|
||||
* @see #preprocessFederatedIdentity(KeycloakSession, RealmModel, IdentityProviderMapperModel, BrokeredIdentityContext)
|
||||
* @see BrokeredIdentityContext#getContextData()
|
||||
*/
|
||||
public static void storeUserProfileForMapper(BrokeredIdentityContext user, JsonNode profile, String provider) {
|
||||
user.getContextData().put(AbstractJsonUserAttributeMapper.CONTEXT_JSON_NODE, profile);
|
||||
if (LOGGER_DUMP_USER_PROFILE.isDebugEnabled())
|
||||
LOGGER_DUMP_USER_PROFILE.debug("User Profile JSON Data for provider "+provider+": " + profile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Import user profile information if it exists in Social provider JSON data into the specified user attribute.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(CONF_USER_ATTRIBUTE);
|
||||
if (attribute == null || attribute.trim().isEmpty()) {
|
||||
logger.warnf("Attribute is not configured for mapper %s", mapperModel.getName());
|
||||
return;
|
||||
}
|
||||
attribute = attribute.trim();
|
||||
|
||||
String value = getJsonValue(mapperModel, context);
|
||||
if (value != null) {
|
||||
context.setUserAttribute(attribute, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
// we do not update user profile from social provider
|
||||
}
|
||||
|
||||
protected static String getJsonValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
String jsonField = mapperModel.getConfig().get(CONF_JSON_FIELD);
|
||||
if (jsonField == null || jsonField.trim().isEmpty()) {
|
||||
logger.warnf("JSON field path is not configured for mapper %s", mapperModel.getName());
|
||||
return null;
|
||||
}
|
||||
jsonField = jsonField.trim();
|
||||
|
||||
if (jsonField.startsWith(JSON_PATH_DELIMITER) || jsonField.endsWith(JSON_PATH_DELIMITER) || jsonField.startsWith("[")) {
|
||||
logger.warnf("JSON field path is invalid %s", jsonField);
|
||||
return null;
|
||||
}
|
||||
|
||||
JsonNode profileJsonNode = (JsonNode) context.getContextData().get(CONTEXT_JSON_NODE);
|
||||
|
||||
String value = getJsonValue(profileJsonNode, jsonField);
|
||||
|
||||
if (value == null) {
|
||||
logger.debugf("User profile JSON value '%s' is not available.", jsonField);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
protected static String getJsonValue(JsonNode baseNode, String fieldPath) {
|
||||
logger.debug("Going to process JsonNode path " + fieldPath + " on data " + baseNode);
|
||||
if (baseNode != null) {
|
||||
|
||||
int idx = fieldPath.indexOf(JSON_PATH_DELIMITER);
|
||||
|
||||
String currentFieldName = fieldPath;
|
||||
if (idx > 0) {
|
||||
currentFieldName = fieldPath.substring(0, idx).trim();
|
||||
if (currentFieldName.isEmpty()) {
|
||||
logger.debug("JSON path is invalid " + fieldPath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String currentNodeName = currentFieldName;
|
||||
int arrayIndex = -1;
|
||||
if (currentFieldName.endsWith("]")) {
|
||||
int bi = currentFieldName.indexOf("[");
|
||||
if (bi == -1) {
|
||||
logger.debug("Invalid array index construct in " + currentFieldName);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String is = currentFieldName.substring(bi+1, currentFieldName.length() - 1).trim();
|
||||
arrayIndex = Integer.parseInt(is);
|
||||
} catch (Exception e) {
|
||||
logger.debug("Invalid array index construct in " + currentFieldName);
|
||||
return null;
|
||||
}
|
||||
currentNodeName = currentFieldName.substring(0,bi).trim();
|
||||
}
|
||||
|
||||
JsonNode currentNode = baseNode.get(currentNodeName);
|
||||
if (arrayIndex > -1 && currentNode.isArray()) {
|
||||
logger.debug("Going to take array node at index " + arrayIndex);
|
||||
currentNode = currentNode.get(arrayIndex);
|
||||
}
|
||||
|
||||
if (currentNode == null) {
|
||||
logger.debug("JsonNode not found for name " + currentFieldName);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (idx < 0) {
|
||||
if (!currentNode.isValueNode()) {
|
||||
logger.debug("JsonNode is not value node for name " + currentFieldName);
|
||||
return null;
|
||||
}
|
||||
String ret = currentNode.asText();
|
||||
if (ret != null && !ret.trim().isEmpty())
|
||||
return ret.trim();
|
||||
} else {
|
||||
return getJsonValue(currentNode, fieldPath.substring(idx + 1));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
+106
-105
@@ -1,105 +1,106 @@
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.HardcodedRoleMapper;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class ClaimToRoleMapper extends AbstractClaimMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
ProviderConfigProperty property1;
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(CLAIM);
|
||||
property1.setLabel("Claim");
|
||||
property1.setHelpText("Name of claim to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'.");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(CLAIM_VALUE);
|
||||
property1.setLabel("Claim Value");
|
||||
property1.setHelpText("Value the claim must have. If the claim is an array, then the value must be contained in the array.");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(HardcodedRoleMapper.ROLE);
|
||||
property.setLabel("Role");
|
||||
property.setHelpText("Role to grant to user if claim is present. To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-role-idp-mapper";
|
||||
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Role Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Claim to Role";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (hasClaimValue(mapperModel, context)) {
|
||||
RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.grantRole(role);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (!hasClaimValue(mapperModel, context)) {
|
||||
RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.deleteRoleMapping(role);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "If a claim exists, grant the user the specified realm or application role.";
|
||||
}
|
||||
|
||||
}
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.HardcodedRoleMapper;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class ClaimToRoleMapper extends AbstractClaimMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
ProviderConfigProperty property1;
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(CLAIM);
|
||||
property1.setLabel("Claim");
|
||||
property1.setHelpText("Name of claim to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'.");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(CLAIM_VALUE);
|
||||
property1.setLabel("Claim Value");
|
||||
property1.setHelpText("Value the claim must have. If the claim is an array, then the value must be contained in the array.");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(HardcodedRoleMapper.ROLE);
|
||||
property.setLabel("Role");
|
||||
property.setHelpText("Role to grant to user if claim is present. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
|
||||
property.setType(ProviderConfigProperty.ROLE_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-role-idp-mapper";
|
||||
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Role Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Claim to Role";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (hasClaimValue(mapperModel, context)) {
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.grantRole(role);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (!hasClaimValue(mapperModel, context)) {
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.deleteRoleMapping(role);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "If a claim exists, grant the user the specified realm or application role.";
|
||||
}
|
||||
|
||||
}
|
||||
+119
-118
@@ -1,118 +1,119 @@
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.HardcodedRoleMapper;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class ExternalKeycloakRoleToRoleMapper extends AbstractClaimMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
private static final String EXTERNAL_ROLE = "external.role";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
ProviderConfigProperty property1;
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(EXTERNAL_ROLE);
|
||||
property1.setLabel("External role");
|
||||
property1.setHelpText("External role to check for. To reference an application role the syntax is appname.approle, i.e. myapp.myrole.");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(HardcodedRoleMapper.ROLE);
|
||||
property.setLabel("Role");
|
||||
property.setHelpText("Role to grant to user if external role is present. To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "keycloak-oidc-role-to-role-idp-mapper";
|
||||
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Role Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "External Role to Role";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
RoleModel role = hasRole(realm, mapperModel, context);
|
||||
if (role != null) {
|
||||
user.grantRole(role);
|
||||
}
|
||||
}
|
||||
|
||||
private RoleModel hasRole(RealmModel realm,IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN);
|
||||
//if (token == null) return;
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
String[] parseRole = HardcodedRoleMapper.parseRole(mapperModel.getConfig().get(EXTERNAL_ROLE));
|
||||
String externalRoleName = parseRole[1];
|
||||
String claimName = null;
|
||||
if (parseRole[0] == null) {
|
||||
claimName = "realm_access.roles";
|
||||
} else {
|
||||
claimName = "resource_access." + parseRole[0] + ".roles";
|
||||
}
|
||||
Object claim = getClaimValue(token, claimName);
|
||||
if (valueEquals(externalRoleName, claim)) {
|
||||
RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
return role;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
RoleModel role = hasRole(realm, mapperModel, context);
|
||||
if (role == null) {
|
||||
user.deleteRoleMapping(role);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Looks for an external role in a keycloak access token. If external role exists, grant the user the specified realm or application role.";
|
||||
}
|
||||
|
||||
}
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.HardcodedRoleMapper;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class ExternalKeycloakRoleToRoleMapper extends AbstractClaimMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
private static final String EXTERNAL_ROLE = "external.role";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
ProviderConfigProperty property1;
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(EXTERNAL_ROLE);
|
||||
property1.setLabel("External role");
|
||||
property1.setHelpText("External role to check for. To reference an application role the syntax is appname.approle, i.e. myapp.myrole.");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(HardcodedRoleMapper.ROLE);
|
||||
property.setLabel("Role");
|
||||
property.setHelpText("Role to grant to user if external role is present. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
|
||||
property.setType(ProviderConfigProperty.ROLE_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "keycloak-oidc-role-to-role-idp-mapper";
|
||||
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Role Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "External Role to Role";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
RoleModel role = hasRole(realm, mapperModel, context);
|
||||
if (role != null) {
|
||||
user.grantRole(role);
|
||||
}
|
||||
}
|
||||
|
||||
private RoleModel hasRole(RealmModel realm,IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN);
|
||||
//if (token == null) return;
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
String[] parseRole = KeycloakModelUtils.parseRole(mapperModel.getConfig().get(EXTERNAL_ROLE));
|
||||
String externalRoleName = parseRole[1];
|
||||
String claimName = null;
|
||||
if (parseRole[0] == null) {
|
||||
claimName = "realm_access.roles";
|
||||
} else {
|
||||
claimName = "resource_access." + parseRole[0] + ".roles";
|
||||
}
|
||||
Object claim = getClaimValue(token, claimName);
|
||||
if (valueEquals(externalRoleName, claim)) {
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
return role;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
RoleModel role = hasRole(realm, mapperModel, context);
|
||||
if (role == null) {
|
||||
user.deleteRoleMapping(role);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Looks for an external role in a keycloak access token. If external role exists, grant the user the specified realm or application role.";
|
||||
}
|
||||
|
||||
}
|
||||
+98
-100
@@ -1,100 +1,98 @@
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UserAttributeMapper extends AbstractClaimMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
public static final String USER_ATTRIBUTE = "user.attribute";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
ProviderConfigProperty property1;
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(CLAIM);
|
||||
property1.setLabel("Claim");
|
||||
property1.setHelpText("Name of claim to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'.");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(USER_ATTRIBUTE);
|
||||
property.setLabel("User Attribute Name");
|
||||
property.setHelpText("User attribute name to store claim.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-user-attribute-idp-mapper";
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
|
||||
Object value = getClaimValue(mapperModel, context);
|
||||
if (value != null) {
|
||||
user.setAttribute(attribute, value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
|
||||
Object value = getClaimValue(mapperModel, context);
|
||||
String current = user.getAttribute(attribute);
|
||||
if (value != null && !value.equals(current)) {
|
||||
user.setAttribute(attribute, value.toString());
|
||||
} else if (value == null) {
|
||||
user.removeAttribute(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Import declared claim if it exists in ID or access token into the specified user attribute.";
|
||||
}
|
||||
|
||||
}
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UserAttributeMapper extends AbstractClaimMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
public static final String USER_ATTRIBUTE = "user.attribute";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
ProviderConfigProperty property1;
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(CLAIM);
|
||||
property1.setLabel("Claim");
|
||||
property1.setHelpText("Name of claim to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'.");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(USER_ATTRIBUTE);
|
||||
property.setLabel("User Attribute Name");
|
||||
property.setHelpText("User attribute name to store claim.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-user-attribute-idp-mapper";
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
|
||||
Object value = getClaimValue(mapperModel, context);
|
||||
if (value != null) {
|
||||
context.setUserAttribute(attribute, value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
|
||||
Object value = getClaimValue(mapperModel, context);
|
||||
String current = user.getFirstAttribute(attribute);
|
||||
if (value != null && !value.equals(current)) {
|
||||
user.setSingleAttribute(attribute, value.toString());
|
||||
} else if (value == null) {
|
||||
user.removeAttribute(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Import declared claim if it exists in ID or access token into the specified user attribute.";
|
||||
}
|
||||
|
||||
}
|
||||
+105
-109
@@ -1,109 +1,105 @@
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UsernameTemplateMapper extends AbstractClaimMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
public static final String TEMPLATE = "template";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(TEMPLATE);
|
||||
property.setLabel("Template");
|
||||
property.setHelpText("Template to use to format the username to import. Substitutions are enclosed in ${}. For example: '${ALIAS}.${CLAIM.sub}'. ALIAS is the provider alias. CLAIM.<NAME> references an ID or Access token claim.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
property.setDefaultValue("${ALIAS}.${CLAIM.preferred_username}");
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-username-idp-mapper";
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Preprocessor";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Username Template Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
}
|
||||
|
||||
static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String template = mapperModel.getConfig().get(TEMPLATE);
|
||||
Matcher m = substitution.matcher(template);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
while (m.find()) {
|
||||
String variable = m.group(1);
|
||||
if (variable.equals("ALIAS")) {
|
||||
m.appendReplacement(sb, context.getIdpConfig().getAlias());
|
||||
} else if (variable.equals("UUID")) {
|
||||
m.appendReplacement(sb, KeycloakModelUtils.generateId());
|
||||
} else if (variable.startsWith("CLAIM.")) {
|
||||
String name = variable.substring("CLAIM.".length());
|
||||
Object value = AbstractClaimMapper.getClaimValue(context, name);
|
||||
if (value == null) value = "";
|
||||
m.appendReplacement(sb, value.toString());
|
||||
} else {
|
||||
m.appendReplacement(sb, m.group(1));
|
||||
}
|
||||
|
||||
}
|
||||
m.appendTail(sb);
|
||||
String username = sb.toString();
|
||||
context.setModelUsername(username);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Format the username to import.";
|
||||
}
|
||||
}
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UsernameTemplateMapper extends AbstractClaimMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
public static final String TEMPLATE = "template";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(TEMPLATE);
|
||||
property.setLabel("Template");
|
||||
property.setHelpText("Template to use to format the username to import. Substitutions are enclosed in ${}. For example: '${ALIAS}.${CLAIM.sub}'. ALIAS is the provider alias. CLAIM.<NAME> references an ID or Access token claim.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
property.setDefaultValue("${ALIAS}.${CLAIM.preferred_username}");
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-username-idp-mapper";
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Preprocessor";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Username Template Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
}
|
||||
|
||||
static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String template = mapperModel.getConfig().get(TEMPLATE);
|
||||
Matcher m = substitution.matcher(template);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
while (m.find()) {
|
||||
String variable = m.group(1);
|
||||
if (variable.equals("ALIAS")) {
|
||||
m.appendReplacement(sb, context.getIdpConfig().getAlias());
|
||||
} else if (variable.equals("UUID")) {
|
||||
m.appendReplacement(sb, KeycloakModelUtils.generateId());
|
||||
} else if (variable.startsWith("CLAIM.")) {
|
||||
String name = variable.substring("CLAIM.".length());
|
||||
Object value = AbstractClaimMapper.getClaimValue(context, name);
|
||||
if (value == null) value = "";
|
||||
m.appendReplacement(sb, value.toString());
|
||||
} else {
|
||||
m.appendReplacement(sb, m.group(1));
|
||||
}
|
||||
|
||||
}
|
||||
m.appendTail(sb);
|
||||
String username = sb.toString();
|
||||
context.setModelUsername(username);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Format the username to import.";
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,32 @@
|
||||
package org.keycloak.broker.oidc.util;
|
||||
|
||||
import org.codehaus.jackson.JsonNode;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class JsonSimpleHttp extends SimpleHttp {
|
||||
public JsonSimpleHttp(String url, String method) {
|
||||
super(url, method);
|
||||
}
|
||||
|
||||
public static JsonSimpleHttp doGet(String url) {
|
||||
return new JsonSimpleHttp(url, "GET");
|
||||
}
|
||||
|
||||
public static JsonSimpleHttp doPost(String url) {
|
||||
return new JsonSimpleHttp(url, "POST");
|
||||
}
|
||||
|
||||
private static ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
public static JsonNode asJson(SimpleHttp request) throws IOException {
|
||||
return mapper.readTree(request.asString());
|
||||
}
|
||||
|
||||
}
|
||||
package org.keycloak.broker.oidc.util;
|
||||
|
||||
import org.codehaus.jackson.JsonNode;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class JsonSimpleHttp extends SimpleHttp {
|
||||
public JsonSimpleHttp(String url, String method) {
|
||||
super(url, method);
|
||||
}
|
||||
|
||||
public static JsonSimpleHttp doGet(String url) {
|
||||
return new JsonSimpleHttp(url, "GET");
|
||||
}
|
||||
|
||||
public static JsonSimpleHttp doPost(String url) {
|
||||
return new JsonSimpleHttp(url, "POST");
|
||||
}
|
||||
|
||||
private static ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
public static JsonNode asJson(SimpleHttp request) throws IOException {
|
||||
return mapper.readTree(request.asString());
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
org.keycloak.broker.oidc.OIDCIdentityProviderFactory
|
||||
org.keycloak.broker.oidc.OIDCIdentityProviderFactory
|
||||
org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
org.keycloak.broker.oidc.mappers.ClaimToRoleMapper
|
||||
org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper
|
||||
org.keycloak.broker.oidc.mappers.UserAttributeMapper
|
||||
org.keycloak.broker.oidc.mappers.ClaimToRoleMapper
|
||||
org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper
|
||||
org.keycloak.broker.oidc.mappers.UserAttributeMapper
|
||||
org.keycloak.broker.oidc.mappers.UsernameTemplateMapper
|
||||
+23
-23
@@ -1,23 +1,23 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-parent</artifactId>
|
||||
<name>Keycloak Broker Parent</name>
|
||||
<description/>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>core</module>
|
||||
<module>oidc</module>
|
||||
<module>saml</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.8.0.CR3</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-parent</artifactId>
|
||||
<name>Keycloak Broker Parent</name>
|
||||
<description/>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>core</module>
|
||||
<module>oidc</module>
|
||||
<module>saml</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
+55
-50
@@ -1,50 +1,55 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-saml</artifactId>
|
||||
<name>Keycloak Broker - SAML Identity Provider</name>
|
||||
<description/>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-broker-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-saml-protocol</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-saml-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-services</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-events-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.8.0.CR3</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-saml</artifactId>
|
||||
<name>Keycloak Broker - SAML Identity Provider</name>
|
||||
<description/>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-broker-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-saml-protocol</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-saml-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-services</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-events-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.keycloak.broker.saml;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.xml.stream.XMLEventReader;
|
||||
|
||||
import org.keycloak.broker.provider.DefaultDataMarshaller;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.saml.common.util.StaxUtil;
|
||||
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
|
||||
import org.keycloak.saml.processing.core.parsers.util.SAMLParserUtil;
|
||||
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLAssertionWriter;
|
||||
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLResponseWriter;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class SAMLDataMarshaller extends DefaultDataMarshaller {
|
||||
|
||||
@Override
|
||||
public String serialize(Object obj) {
|
||||
|
||||
// Lame impl, but hopefully sufficient for now. See if something better is needed...
|
||||
if (obj.getClass().getName().startsWith("org.keycloak.dom.saml")) {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
if (obj instanceof ResponseType) {
|
||||
ResponseType responseType = (ResponseType) obj;
|
||||
SAMLResponseWriter samlWriter = new SAMLResponseWriter(StaxUtil.getXMLStreamWriter(bos));
|
||||
samlWriter.write(responseType);
|
||||
} else if (obj instanceof AssertionType) {
|
||||
AssertionType assertion = (AssertionType) obj;
|
||||
SAMLAssertionWriter samlWriter = new SAMLAssertionWriter(StaxUtil.getXMLStreamWriter(bos));
|
||||
samlWriter.write(assertion);
|
||||
} else if (obj instanceof AuthnStatementType) {
|
||||
AuthnStatementType authnStatement = (AuthnStatementType) obj;
|
||||
SAMLAssertionWriter samlWriter = new SAMLAssertionWriter(StaxUtil.getXMLStreamWriter(bos));
|
||||
samlWriter.write(authnStatement, true);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Don't know how to serialize object of type " + obj.getClass().getName());
|
||||
}
|
||||
} catch (ProcessingException pe) {
|
||||
throw new RuntimeException(pe);
|
||||
}
|
||||
|
||||
return new String(bos.toByteArray());
|
||||
} else {
|
||||
return super.serialize(obj);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T deserialize(String serialized, Class<T> clazz) {
|
||||
if (clazz.getName().startsWith("org.keycloak.dom.saml")) {
|
||||
String xmlString = serialized;
|
||||
|
||||
try {
|
||||
if (clazz.equals(ResponseType.class) || clazz.equals(AssertionType.class)) {
|
||||
byte[] bytes = xmlString.getBytes();
|
||||
InputStream is = new ByteArrayInputStream(bytes);
|
||||
Object respType = new SAMLParser().parse(is);
|
||||
return clazz.cast(respType);
|
||||
} else if (clazz.equals(AuthnStatementType.class)) {
|
||||
byte[] bytes = xmlString.getBytes();
|
||||
InputStream is = new ByteArrayInputStream(bytes);
|
||||
XMLEventReader xmlEventReader = new SAMLParser().createEventReader(is);
|
||||
AuthnStatementType authnStatement = SAMLParserUtil.parseAuthnStatement(xmlEventReader);
|
||||
return clazz.cast(authnStatement);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Don't know how to deserialize object of type " + clazz.getName());
|
||||
}
|
||||
} catch (ParsingException pe) {
|
||||
throw new RuntimeException(pe);
|
||||
}
|
||||
|
||||
} else {
|
||||
return super.deserialize(serialized, clazz);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,491 +1,447 @@
|
||||
package org.keycloak.broker.saml;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.VerificationException;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.broker.provider.IdentityProvider;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.EncryptedAssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.assertion.SubjectType;
|
||||
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.saml.SAML2LogoutResponseBuilder;
|
||||
import org.keycloak.protocol.saml.SAMLRequestParser;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.protocol.saml.SamlProtocolUtils;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLConstants;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.saml.common.util.DocumentUtil;
|
||||
import org.keycloak.saml.common.util.StaxParserUtil;
|
||||
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
|
||||
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
|
||||
import org.keycloak.saml.processing.core.util.JAXPValidationUtil;
|
||||
import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
|
||||
import org.keycloak.saml.processing.core.util.XMLSignatureUtil;
|
||||
import org.keycloak.saml.processing.web.util.PostBindingUtil;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.FormParam;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import javax.xml.namespace.QName;
|
||||
import java.io.IOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class SAMLEndpoint {
|
||||
protected static final Logger logger = Logger.getLogger(SAMLEndpoint.class);
|
||||
public static final String SAML_FEDERATED_SESSION_INDEX = "SAML_FEDERATED_SESSION_INDEX";
|
||||
public static final String SAML_FEDERATED_SUBJECT = "SAML_FEDERATED_SUBJECT";
|
||||
public static final String SAML_FEDERATED_SUBJECT_NAMEFORMAT = "SAML_FEDERATED_SUBJECT_NAMEFORMAT";
|
||||
public static final String SAML_LOGIN_RESPONSE = "SAML_LOGIN_RESPONSE";
|
||||
public static final String SAML_ASSERTION = "SAML_ASSERTION";
|
||||
public static final String SAML_AUTHN_STATEMENT = "SAML_AUTHN_STATEMENT";
|
||||
protected RealmModel realm;
|
||||
protected EventBuilder event;
|
||||
protected SAMLIdentityProviderConfig config;
|
||||
protected IdentityProvider.AuthenticationCallback callback;
|
||||
protected SAMLIdentityProvider provider;
|
||||
|
||||
@Context
|
||||
private UriInfo uriInfo;
|
||||
|
||||
@Context
|
||||
private KeycloakSession session;
|
||||
|
||||
@Context
|
||||
private ClientConnection clientConnection;
|
||||
|
||||
@Context
|
||||
private HttpHeaders headers;
|
||||
|
||||
|
||||
public SAMLEndpoint(RealmModel realm, SAMLIdentityProvider provider, SAMLIdentityProviderConfig config, IdentityProvider.AuthenticationCallback callback) {
|
||||
this.realm = realm;
|
||||
this.config = config;
|
||||
this.callback = callback;
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
@GET
|
||||
public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
|
||||
@QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
|
||||
@QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
return new RedirectBinding().execute(samlRequest, samlResponse, relayState);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
|
||||
@FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
|
||||
@FormParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
return new PostBinding().execute(samlRequest, samlResponse, relayState);
|
||||
}
|
||||
|
||||
protected abstract class Binding {
|
||||
private boolean checkSsl() {
|
||||
if (uriInfo.getBaseUri().getScheme().equals("https")) {
|
||||
return true;
|
||||
} else {
|
||||
return !realm.getSslRequired().isRequired(clientConnection);
|
||||
}
|
||||
}
|
||||
|
||||
protected Response basicChecks(String samlRequest, String samlResponse) {
|
||||
if (!checkSsl()) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.SSL_REQUIRED);
|
||||
return ErrorPage.error(session, Messages.HTTPS_REQUIRED);
|
||||
}
|
||||
if (!realm.isEnabled()) {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.REALM_DISABLED);
|
||||
return ErrorPage.error(session, Messages.REALM_NOT_ENABLED);
|
||||
}
|
||||
|
||||
if (samlRequest == null && samlResponse == null) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
return ErrorPage.error(session, Messages.INVALID_REQUEST);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract String getBindingType();
|
||||
protected abstract void verifySignature(SAMLDocumentHolder documentHolder) throws VerificationException;
|
||||
protected abstract SAMLDocumentHolder extractRequestDocument(String samlRequest);
|
||||
protected abstract SAMLDocumentHolder extractResponseDocument(String response);
|
||||
protected PublicKey getIDPKey() {
|
||||
X509Certificate certificate = null;
|
||||
try {
|
||||
certificate = XMLSignatureUtil.getX509CertificateFromKeyInfoString(config.getSigningCertificate().replaceAll("\\s", ""));
|
||||
} catch (ProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return certificate.getPublicKey();
|
||||
}
|
||||
|
||||
public Response execute(String samlRequest, String samlResponse, String relayState) {
|
||||
event = new EventBuilder(realm, session, clientConnection);
|
||||
Response response = basicChecks(samlRequest, samlResponse);
|
||||
if (response != null) return response;
|
||||
if (samlRequest != null) return handleSamlRequest(samlRequest, relayState);
|
||||
else return handleSamlResponse(samlResponse, relayState);
|
||||
}
|
||||
|
||||
protected Response handleSamlRequest(String samlRequest, String relayState) {
|
||||
SAMLDocumentHolder holder = extractRequestDocument(samlRequest);
|
||||
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
|
||||
// validate destination
|
||||
if (requestAbstractType.getDestination() != null && !uriInfo.getAbsolutePath().equals(requestAbstractType.getDestination())) {
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.detail(Details.REASON, "invalid_destination");
|
||||
event.error(Errors.INVALID_SAML_RESPONSE);
|
||||
return ErrorPage.error(session, Messages.INVALID_REQUEST);
|
||||
}
|
||||
if (config.isValidateSignature()) {
|
||||
try {
|
||||
verifySignature(holder);
|
||||
} catch (VerificationException e) {
|
||||
logger.error("validation failed", e);
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.error(Errors.INVALID_SIGNATURE);
|
||||
return ErrorPage.error(session, Messages.INVALID_REQUESTER);
|
||||
}
|
||||
}
|
||||
|
||||
if (requestAbstractType instanceof LogoutRequestType) {
|
||||
logger.debug("** logout request");
|
||||
event.event(EventType.LOGOUT);
|
||||
LogoutRequestType logout = (LogoutRequestType) requestAbstractType;
|
||||
return logoutRequest(logout, relayState);
|
||||
|
||||
} else {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return ErrorPage.error(session, Messages.INVALID_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
protected Response logoutRequest(LogoutRequestType request, String relayState) {
|
||||
String brokerUserId = config.getAlias() + "." + request.getNameID().getValue();
|
||||
if (request.getSessionIndex() == null || request.getSessionIndex().isEmpty()) {
|
||||
List<UserSessionModel> userSessions = session.sessions().getUserSessionByBrokerUserId(realm, brokerUserId);
|
||||
for (UserSessionModel userSession : userSessions) {
|
||||
if (userSession.getState() == UserSessionModel.State.LOGGING_OUT || userSession.getState() == UserSessionModel.State.LOGGED_OUT) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, false);
|
||||
} catch (Exception e) {
|
||||
logger.warn("failed to do backchannel logout for userSession", e);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
for (String sessionIndex : request.getSessionIndex()) {
|
||||
String brokerSessionId = brokerUserId + "." + sessionIndex;
|
||||
UserSessionModel userSession = session.sessions().getUserSessionByBrokerSessionId(realm, brokerSessionId);
|
||||
if (userSession != null) {
|
||||
if (userSession.getState() == UserSessionModel.State.LOGGING_OUT || userSession.getState() == UserSessionModel.State.LOGGED_OUT) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, false);
|
||||
} catch (Exception e) {
|
||||
logger.warn("failed to do backchannel logout for userSession", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String issuerURL = getEntityId(uriInfo, realm);
|
||||
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
|
||||
builder.logoutRequestID(request.getID());
|
||||
builder.destination(config.getSingleLogoutServiceUrl());
|
||||
builder.issuer(issuerURL);
|
||||
builder.relayState(relayState);
|
||||
if (config.isWantAuthnRequestsSigned()) {
|
||||
builder.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
|
||||
.signDocument();
|
||||
}
|
||||
try {
|
||||
if (config.isPostBindingResponse()) {
|
||||
return builder.postBinding().response();
|
||||
} else {
|
||||
return builder.redirectBinding().response();
|
||||
}
|
||||
} catch (ConfigurationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (ProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String getEntityId(UriInfo uriInfo, RealmModel realm) {
|
||||
return UriBuilder.fromUri(uriInfo.getBaseUri()).path("realms").path(realm.getName()).build().toString();
|
||||
}
|
||||
protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder holder, ResponseType responseType, String relayState) {
|
||||
|
||||
try {
|
||||
AssertionType assertion = getAssertion(responseType);
|
||||
SubjectType subject = assertion.getSubject();
|
||||
SubjectType.STSubType subType = subject.getSubType();
|
||||
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
|
||||
//Map<String, String> notes = new HashMap<>();
|
||||
BrokeredIdentityContext identity = new BrokeredIdentityContext(subjectNameID.getValue());
|
||||
identity.setCode(relayState);
|
||||
identity.getContextData().put(SAML_LOGIN_RESPONSE, responseType);
|
||||
identity.getContextData().put(SAML_ASSERTION, assertion);
|
||||
|
||||
identity.setUsername(subjectNameID.getValue());
|
||||
|
||||
if (subjectNameID.getFormat().toString().equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) {
|
||||
identity.setEmail(subjectNameID.getValue());
|
||||
}
|
||||
|
||||
if (config.isStoreToken()) {
|
||||
identity.setToken(samlResponse);
|
||||
}
|
||||
|
||||
AuthnStatementType authn = null;
|
||||
for (Object statement : assertion.getStatements()) {
|
||||
if (statement instanceof AuthnStatementType) {
|
||||
authn = (AuthnStatementType)statement;
|
||||
identity.getContextData().put(SAML_AUTHN_STATEMENT, authn);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (assertion.getAttributeStatements() != null ) {
|
||||
for (AttributeStatementType attrStatement : assertion.getAttributeStatements()) {
|
||||
for (AttributeStatementType.ASTChoiceType choice : attrStatement.getAttributes()) {
|
||||
AttributeType attribute = choice.getAttribute();
|
||||
if (X500SAMLProfileConstants.EMAIL.getFriendlyName().equals(attribute.getFriendlyName())
|
||||
|| X500SAMLProfileConstants.EMAIL.get().equals(attribute.getName())) {
|
||||
if (!attribute.getAttributeValue().isEmpty()) identity.setEmail(attribute.getAttributeValue().get(0).toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
String brokerUserId = config.getAlias() + "." + subjectNameID.getValue();
|
||||
identity.setBrokerUserId(brokerUserId);
|
||||
identity.setIdpConfig(config);
|
||||
identity.setIdp(provider);
|
||||
if (authn != null && authn.getSessionIndex() != null) {
|
||||
identity.setBrokerSessionId(identity.getBrokerUserId() + "." + authn.getSessionIndex());
|
||||
}
|
||||
|
||||
|
||||
return callback.authenticated(identity);
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new IdentityBrokerException("Could not process response from SAML identity provider.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private AssertionType getAssertion(ResponseType responseType) throws ProcessingException {
|
||||
List<ResponseType.RTChoiceType> assertions = responseType.getAssertions();
|
||||
|
||||
if (assertions.isEmpty()) {
|
||||
throw new IdentityBrokerException("No assertion from response.");
|
||||
}
|
||||
|
||||
ResponseType.RTChoiceType rtChoiceType = assertions.get(0);
|
||||
EncryptedAssertionType encryptedAssertion = rtChoiceType.getEncryptedAssertion();
|
||||
|
||||
if (encryptedAssertion != null) {
|
||||
decryptAssertion(responseType, realm.getPrivateKey());
|
||||
|
||||
}
|
||||
return responseType.getAssertions().get(0).getAssertion();
|
||||
}
|
||||
|
||||
public Response handleSamlResponse(String samlResponse, String relayState) {
|
||||
SAMLDocumentHolder holder = extractResponseDocument(samlResponse);
|
||||
StatusResponseType statusResponse = (StatusResponseType)holder.getSamlObject();
|
||||
// validate destination
|
||||
if (statusResponse.getDestination() != null && !uriInfo.getAbsolutePath().toString().equals(statusResponse.getDestination())) {
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.detail(Details.REASON, "invalid_destination");
|
||||
event.error(Errors.INVALID_SAML_RESPONSE);
|
||||
return ErrorPage.error(session, Messages.INVALID_FEDERATED_IDENTITY_ACTION);
|
||||
}
|
||||
if (config.isValidateSignature()) {
|
||||
try {
|
||||
verifySignature(holder);
|
||||
} catch (VerificationException e) {
|
||||
logger.error("validation failed", e);
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.error(Errors.INVALID_SIGNATURE);
|
||||
return ErrorPage.error(session, Messages.INVALID_FEDERATED_IDENTITY_ACTION);
|
||||
}
|
||||
}
|
||||
if (statusResponse instanceof ResponseType) {
|
||||
return handleLoginResponse(samlResponse, holder, (ResponseType)statusResponse, relayState);
|
||||
|
||||
} else {
|
||||
// todo need to check that it is actually a LogoutResponse
|
||||
return handleLogoutResponse(holder, statusResponse, relayState);
|
||||
}
|
||||
//throw new RuntimeException("Unknown response type");
|
||||
|
||||
}
|
||||
|
||||
protected Response handleLogoutResponse(SAMLDocumentHolder holder, StatusResponseType responseType, String relayState) {
|
||||
if (relayState == null) {
|
||||
logger.error("no valid user session");
|
||||
event.event(EventType.LOGOUT);
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
return ErrorPage.error(session, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||
}
|
||||
UserSessionModel userSession = session.sessions().getUserSession(realm, relayState);
|
||||
if (userSession == null) {
|
||||
logger.error("no valid user session");
|
||||
event.event(EventType.LOGOUT);
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
return ErrorPage.error(session, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||
}
|
||||
if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
|
||||
logger.error("usersession in different state");
|
||||
event.event(EventType.LOGOUT);
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
return ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE);
|
||||
}
|
||||
return AuthenticationManager.finishBrowserLogout(session, realm, userSession, uriInfo, clientConnection, headers);
|
||||
}
|
||||
|
||||
|
||||
protected ResponseType decryptAssertion(ResponseType responseType, PrivateKey privateKey) throws ProcessingException {
|
||||
SAML2Response saml2Response = new SAML2Response();
|
||||
|
||||
try {
|
||||
Document doc = saml2Response.convert(responseType);
|
||||
Element enc = DocumentUtil.getElement(doc, new QName(JBossSAMLConstants.ENCRYPTED_ASSERTION.get()));
|
||||
|
||||
if (enc == null) {
|
||||
throw new IdentityBrokerException("No encrypted assertion found.");
|
||||
}
|
||||
|
||||
String oldID = enc.getAttribute(JBossSAMLConstants.ID.get());
|
||||
Document newDoc = DocumentUtil.createDocument();
|
||||
Node importedNode = newDoc.importNode(enc, true);
|
||||
newDoc.appendChild(importedNode);
|
||||
|
||||
Element decryptedDocumentElement = XMLEncryptionUtil.decryptElementInDocument(newDoc, privateKey);
|
||||
SAMLParser parser = new SAMLParser();
|
||||
|
||||
JAXPValidationUtil.checkSchemaValidation(decryptedDocumentElement);
|
||||
AssertionType assertion = (AssertionType) parser.parse(StaxParserUtil.getXMLEventReader(DocumentUtil
|
||||
.getNodeAsStream(decryptedDocumentElement)));
|
||||
|
||||
responseType.replaceAssertion(oldID, new ResponseType.RTChoiceType(assertion));
|
||||
|
||||
return responseType;
|
||||
} catch (Exception e) {
|
||||
throw new IdentityBrokerException("Could not decrypt assertion.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected class PostBinding extends Binding {
|
||||
@Override
|
||||
protected void verifySignature(SAMLDocumentHolder documentHolder) throws VerificationException {
|
||||
SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
|
||||
return SAMLRequestParser.parseRequestPostBinding(samlRequest);
|
||||
}
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractResponseDocument(String response) {
|
||||
byte[] samlBytes = PostBindingUtil.base64Decode(response);
|
||||
String xml = new String(samlBytes);
|
||||
return SAMLRequestParser.parseResponseDocument(samlBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getBindingType() {
|
||||
return SamlProtocol.SAML_POST_BINDING;
|
||||
}
|
||||
}
|
||||
|
||||
protected class RedirectBinding extends Binding {
|
||||
@Override
|
||||
protected void verifySignature(SAMLDocumentHolder documentHolder) throws VerificationException {
|
||||
PublicKey publicKey = getIDPKey();
|
||||
SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
|
||||
return SAMLRequestParser.parseRequestRedirectBinding(samlRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractResponseDocument(String response) {
|
||||
return SAMLRequestParser.parseRequestRedirectBinding(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getBindingType() {
|
||||
return SamlProtocol.SAML_REDIRECT_BINDING;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
package org.keycloak.broker.saml;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.broker.provider.IdentityProvider;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.assertion.SubjectType;
|
||||
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
|
||||
import org.keycloak.saml.SAML2LogoutResponseBuilder;
|
||||
import org.keycloak.saml.SAMLRequestParser;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.protocol.saml.SamlProtocolUtils;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
|
||||
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
|
||||
import org.keycloak.saml.processing.core.util.XMLSignatureUtil;
|
||||
import org.keycloak.saml.processing.web.util.PostBindingUtil;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.FormParam;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.io.IOException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class SAMLEndpoint {
|
||||
protected static final Logger logger = Logger.getLogger(SAMLEndpoint.class);
|
||||
public static final String SAML_FEDERATED_SESSION_INDEX = "SAML_FEDERATED_SESSION_INDEX";
|
||||
public static final String SAML_FEDERATED_SUBJECT = "SAML_FEDERATED_SUBJECT";
|
||||
public static final String SAML_FEDERATED_SUBJECT_NAMEFORMAT = "SAML_FEDERATED_SUBJECT_NAMEFORMAT";
|
||||
public static final String SAML_LOGIN_RESPONSE = "SAML_LOGIN_RESPONSE";
|
||||
public static final String SAML_ASSERTION = "SAML_ASSERTION";
|
||||
public static final String SAML_AUTHN_STATEMENT = "SAML_AUTHN_STATEMENT";
|
||||
protected RealmModel realm;
|
||||
protected EventBuilder event;
|
||||
protected SAMLIdentityProviderConfig config;
|
||||
protected IdentityProvider.AuthenticationCallback callback;
|
||||
protected SAMLIdentityProvider provider;
|
||||
|
||||
@Context
|
||||
private UriInfo uriInfo;
|
||||
|
||||
@Context
|
||||
private KeycloakSession session;
|
||||
|
||||
@Context
|
||||
private ClientConnection clientConnection;
|
||||
|
||||
@Context
|
||||
private HttpHeaders headers;
|
||||
|
||||
|
||||
public SAMLEndpoint(RealmModel realm, SAMLIdentityProvider provider, SAMLIdentityProviderConfig config, IdentityProvider.AuthenticationCallback callback) {
|
||||
this.realm = realm;
|
||||
this.config = config;
|
||||
this.callback = callback;
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
@GET
|
||||
@NoCache
|
||||
@Path("descriptor")
|
||||
public Response getSPDescriptor() {
|
||||
return provider.export(uriInfo, realm, null);
|
||||
}
|
||||
|
||||
@GET
|
||||
public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
|
||||
@QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
|
||||
@QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
return new RedirectBinding().execute(samlRequest, samlResponse, relayState);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
|
||||
@FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
|
||||
@FormParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
return new PostBinding().execute(samlRequest, samlResponse, relayState);
|
||||
}
|
||||
|
||||
protected abstract class Binding {
|
||||
private boolean checkSsl() {
|
||||
if (uriInfo.getBaseUri().getScheme().equals("https")) {
|
||||
return true;
|
||||
} else {
|
||||
return !realm.getSslRequired().isRequired(clientConnection);
|
||||
}
|
||||
}
|
||||
|
||||
protected Response basicChecks(String samlRequest, String samlResponse) {
|
||||
if (!checkSsl()) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.SSL_REQUIRED);
|
||||
return ErrorPage.error(session, Messages.HTTPS_REQUIRED);
|
||||
}
|
||||
if (!realm.isEnabled()) {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.REALM_DISABLED);
|
||||
return ErrorPage.error(session, Messages.REALM_NOT_ENABLED);
|
||||
}
|
||||
|
||||
if (samlRequest == null && samlResponse == null) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
return ErrorPage.error(session, Messages.INVALID_REQUEST);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract String getBindingType();
|
||||
protected abstract void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException;
|
||||
protected abstract SAMLDocumentHolder extractRequestDocument(String samlRequest);
|
||||
protected abstract SAMLDocumentHolder extractResponseDocument(String response);
|
||||
protected PublicKey getIDPKey() {
|
||||
X509Certificate certificate = null;
|
||||
try {
|
||||
certificate = XMLSignatureUtil.getX509CertificateFromKeyInfoString(config.getSigningCertificate().replaceAll("\\s", ""));
|
||||
} catch (ProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return certificate.getPublicKey();
|
||||
}
|
||||
|
||||
public Response execute(String samlRequest, String samlResponse, String relayState) {
|
||||
event = new EventBuilder(realm, session, clientConnection);
|
||||
Response response = basicChecks(samlRequest, samlResponse);
|
||||
if (response != null) return response;
|
||||
if (samlRequest != null) return handleSamlRequest(samlRequest, relayState);
|
||||
else return handleSamlResponse(samlResponse, relayState);
|
||||
}
|
||||
|
||||
protected Response handleSamlRequest(String samlRequest, String relayState) {
|
||||
SAMLDocumentHolder holder = extractRequestDocument(samlRequest);
|
||||
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
|
||||
// validate destination
|
||||
if (requestAbstractType.getDestination() != null && !uriInfo.getAbsolutePath().equals(requestAbstractType.getDestination())) {
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.detail(Details.REASON, "invalid_destination");
|
||||
event.error(Errors.INVALID_SAML_RESPONSE);
|
||||
return ErrorPage.error(session, Messages.INVALID_REQUEST);
|
||||
}
|
||||
if (config.isValidateSignature()) {
|
||||
try {
|
||||
verifySignature(GeneralConstants.SAML_REQUEST_KEY, holder);
|
||||
} catch (VerificationException e) {
|
||||
logger.error("validation failed", e);
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.error(Errors.INVALID_SIGNATURE);
|
||||
return ErrorPage.error(session, Messages.INVALID_REQUESTER);
|
||||
}
|
||||
}
|
||||
|
||||
if (requestAbstractType instanceof LogoutRequestType) {
|
||||
logger.debug("** logout request");
|
||||
event.event(EventType.LOGOUT);
|
||||
LogoutRequestType logout = (LogoutRequestType) requestAbstractType;
|
||||
return logoutRequest(logout, relayState);
|
||||
|
||||
} else {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return ErrorPage.error(session, Messages.INVALID_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
protected Response logoutRequest(LogoutRequestType request, String relayState) {
|
||||
String brokerUserId = config.getAlias() + "." + request.getNameID().getValue();
|
||||
if (request.getSessionIndex() == null || request.getSessionIndex().isEmpty()) {
|
||||
List<UserSessionModel> userSessions = session.sessions().getUserSessionByBrokerUserId(realm, brokerUserId);
|
||||
for (UserSessionModel userSession : userSessions) {
|
||||
if (userSession.getState() == UserSessionModel.State.LOGGING_OUT || userSession.getState() == UserSessionModel.State.LOGGED_OUT) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, false);
|
||||
} catch (Exception e) {
|
||||
logger.warn("failed to do backchannel logout for userSession", e);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
for (String sessionIndex : request.getSessionIndex()) {
|
||||
String brokerSessionId = brokerUserId + "." + sessionIndex;
|
||||
UserSessionModel userSession = session.sessions().getUserSessionByBrokerSessionId(realm, brokerSessionId);
|
||||
if (userSession != null) {
|
||||
if (userSession.getState() == UserSessionModel.State.LOGGING_OUT || userSession.getState() == UserSessionModel.State.LOGGED_OUT) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, false);
|
||||
} catch (Exception e) {
|
||||
logger.warn("failed to do backchannel logout for userSession", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String issuerURL = getEntityId(uriInfo, realm);
|
||||
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
|
||||
builder.logoutRequestID(request.getID());
|
||||
builder.destination(config.getSingleLogoutServiceUrl());
|
||||
builder.issuer(issuerURL);
|
||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
|
||||
.relayState(relayState);
|
||||
if (config.isWantAuthnRequestsSigned()) {
|
||||
binding.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
|
||||
.signatureAlgorithm(provider.getSignatureAlgorithm())
|
||||
.signDocument();
|
||||
}
|
||||
try {
|
||||
if (config.isPostBindingResponse()) {
|
||||
return binding.postBinding(builder.buildDocument()).response(config.getSingleLogoutServiceUrl());
|
||||
} else {
|
||||
return binding.redirectBinding(builder.buildDocument()).response(config.getSingleLogoutServiceUrl());
|
||||
}
|
||||
} catch (ConfigurationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (ProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String getEntityId(UriInfo uriInfo, RealmModel realm) {
|
||||
return UriBuilder.fromUri(uriInfo.getBaseUri()).path("realms").path(realm.getName()).build().toString();
|
||||
}
|
||||
protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder holder, ResponseType responseType, String relayState) {
|
||||
|
||||
try {
|
||||
AssertionType assertion = AssertionUtil.getAssertion(responseType, realm.getPrivateKey());
|
||||
SubjectType subject = assertion.getSubject();
|
||||
SubjectType.STSubType subType = subject.getSubType();
|
||||
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
|
||||
//Map<String, String> notes = new HashMap<>();
|
||||
BrokeredIdentityContext identity = new BrokeredIdentityContext(subjectNameID.getValue());
|
||||
identity.setCode(relayState);
|
||||
identity.getContextData().put(SAML_LOGIN_RESPONSE, responseType);
|
||||
identity.getContextData().put(SAML_ASSERTION, assertion);
|
||||
|
||||
identity.setUsername(subjectNameID.getValue());
|
||||
|
||||
//SAML Spec 2.2.2 Format is optional
|
||||
if (subjectNameID.getFormat() != null && subjectNameID.getFormat().toString().equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) {
|
||||
identity.setEmail(subjectNameID.getValue());
|
||||
}
|
||||
|
||||
if (config.isStoreToken()) {
|
||||
identity.setToken(samlResponse);
|
||||
}
|
||||
|
||||
AuthnStatementType authn = null;
|
||||
for (Object statement : assertion.getStatements()) {
|
||||
if (statement instanceof AuthnStatementType) {
|
||||
authn = (AuthnStatementType)statement;
|
||||
identity.getContextData().put(SAML_AUTHN_STATEMENT, authn);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (assertion.getAttributeStatements() != null ) {
|
||||
for (AttributeStatementType attrStatement : assertion.getAttributeStatements()) {
|
||||
for (AttributeStatementType.ASTChoiceType choice : attrStatement.getAttributes()) {
|
||||
AttributeType attribute = choice.getAttribute();
|
||||
if (X500SAMLProfileConstants.EMAIL.getFriendlyName().equals(attribute.getFriendlyName())
|
||||
|| X500SAMLProfileConstants.EMAIL.get().equals(attribute.getName())) {
|
||||
if (!attribute.getAttributeValue().isEmpty()) identity.setEmail(attribute.getAttributeValue().get(0).toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
String brokerUserId = config.getAlias() + "." + subjectNameID.getValue();
|
||||
identity.setBrokerUserId(brokerUserId);
|
||||
identity.setIdpConfig(config);
|
||||
identity.setIdp(provider);
|
||||
if (authn != null && authn.getSessionIndex() != null) {
|
||||
identity.setBrokerSessionId(identity.getBrokerUserId() + "." + authn.getSessionIndex());
|
||||
}
|
||||
|
||||
|
||||
return callback.authenticated(identity);
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new IdentityBrokerException("Could not process response from SAML identity provider.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public Response handleSamlResponse(String samlResponse, String relayState) {
|
||||
SAMLDocumentHolder holder = extractResponseDocument(samlResponse);
|
||||
StatusResponseType statusResponse = (StatusResponseType)holder.getSamlObject();
|
||||
// validate destination
|
||||
if (statusResponse.getDestination() != null && !uriInfo.getAbsolutePath().toString().equals(statusResponse.getDestination())) {
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.detail(Details.REASON, "invalid_destination");
|
||||
event.error(Errors.INVALID_SAML_RESPONSE);
|
||||
return ErrorPage.error(session, Messages.INVALID_FEDERATED_IDENTITY_ACTION);
|
||||
}
|
||||
if (config.isValidateSignature()) {
|
||||
try {
|
||||
verifySignature(GeneralConstants.SAML_RESPONSE_KEY, holder);
|
||||
} catch (VerificationException e) {
|
||||
logger.error("validation failed", e);
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.error(Errors.INVALID_SIGNATURE);
|
||||
return ErrorPage.error(session, Messages.INVALID_FEDERATED_IDENTITY_ACTION);
|
||||
}
|
||||
}
|
||||
if (statusResponse instanceof ResponseType) {
|
||||
return handleLoginResponse(samlResponse, holder, (ResponseType)statusResponse, relayState);
|
||||
|
||||
} else {
|
||||
// todo need to check that it is actually a LogoutResponse
|
||||
return handleLogoutResponse(holder, statusResponse, relayState);
|
||||
}
|
||||
//throw new RuntimeException("Unknown response type");
|
||||
|
||||
}
|
||||
|
||||
protected Response handleLogoutResponse(SAMLDocumentHolder holder, StatusResponseType responseType, String relayState) {
|
||||
if (relayState == null) {
|
||||
logger.error("no valid user session");
|
||||
event.event(EventType.LOGOUT);
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
return ErrorPage.error(session, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||
}
|
||||
UserSessionModel userSession = session.sessions().getUserSession(realm, relayState);
|
||||
if (userSession == null) {
|
||||
logger.error("no valid user session");
|
||||
event.event(EventType.LOGOUT);
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
return ErrorPage.error(session, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||
}
|
||||
if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
|
||||
logger.error("usersession in different state");
|
||||
event.event(EventType.LOGOUT);
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
return ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE);
|
||||
}
|
||||
return AuthenticationManager.finishBrowserLogout(session, realm, userSession, uriInfo, clientConnection, headers);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected class PostBinding extends Binding {
|
||||
@Override
|
||||
protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
|
||||
SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
|
||||
return SAMLRequestParser.parseRequestPostBinding(samlRequest);
|
||||
}
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractResponseDocument(String response) {
|
||||
byte[] samlBytes = PostBindingUtil.base64Decode(response);
|
||||
String xml = new String(samlBytes);
|
||||
return SAMLRequestParser.parseResponseDocument(samlBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getBindingType() {
|
||||
return SamlProtocol.SAML_POST_BINDING;
|
||||
}
|
||||
}
|
||||
|
||||
protected class RedirectBinding extends Binding {
|
||||
@Override
|
||||
protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
|
||||
PublicKey publicKey = getIDPKey();
|
||||
SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo, key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
|
||||
return SAMLRequestParser.parseRequestRedirectBinding(samlRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractResponseDocument(String response) {
|
||||
return SAMLRequestParser.parseRequestRedirectBinding(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getBindingType() {
|
||||
return SamlProtocol.SAML_REDIRECT_BINDING;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
|
||||
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.broker.provider.IdentityProviderDataMarshaller;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
||||
@@ -33,20 +34,18 @@ import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.saml.SAML2AuthnRequestBuilder;
|
||||
import org.keycloak.protocol.saml.SAML2LogoutRequestBuilder;
|
||||
import org.keycloak.protocol.saml.SAML2NameIDPolicyBuilder;
|
||||
import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
|
||||
import org.keycloak.saml.SAML2AuthnRequestBuilder;
|
||||
import org.keycloak.saml.SAML2LogoutRequestBuilder;
|
||||
import org.keycloak.saml.SAML2NameIDPolicyBuilder;
|
||||
import org.keycloak.saml.SignatureAlgorithm;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
@@ -92,7 +91,8 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||
.issuer(issuerURL)
|
||||
.forceAuthn(getConfig().isForceAuthn())
|
||||
.protocolBinding(protocolBinding)
|
||||
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat))
|
||||
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
|
||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
|
||||
.relayState(request.getState());
|
||||
|
||||
if (getConfig().isWantAuthnRequestsSigned()) {
|
||||
@@ -109,14 +109,15 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||
|
||||
KeyPair keypair = new KeyPair(publicKey, privateKey);
|
||||
|
||||
authnRequestBuilder.signWith(keypair);
|
||||
authnRequestBuilder.signDocument();
|
||||
binding.signWith(keypair);
|
||||
binding.signatureAlgorithm(getSignatureAlgorithm());
|
||||
binding.signDocument();
|
||||
}
|
||||
|
||||
if (getConfig().isPostBindingAuthnRequest()) {
|
||||
return authnRequestBuilder.postBinding().request();
|
||||
return binding.postBinding(authnRequestBuilder.toDocument()).request(destinationUrl);
|
||||
} else {
|
||||
return authnRequestBuilder.redirectBinding().request();
|
||||
return binding.redirectBinding(authnRequestBuilder.toDocument()).request(destinationUrl);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IdentityBrokerException("Could not create authentication request.", e);
|
||||
@@ -153,9 +154,10 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||
String singleLogoutServiceUrl = getConfig().getSingleLogoutServiceUrl();
|
||||
if (singleLogoutServiceUrl == null || singleLogoutServiceUrl.trim().equals("") || !getConfig().isBackchannelSupported()) return;
|
||||
SAML2LogoutRequestBuilder logoutBuilder = buildLogoutRequest(userSession, uriInfo, realm, singleLogoutServiceUrl);
|
||||
JaxrsSAML2BindingBuilder binding = buildLogoutBinding(userSession, realm);
|
||||
try {
|
||||
int status = SimpleHttp.doPost(singleLogoutServiceUrl)
|
||||
.param(GeneralConstants.SAML_REQUEST_KEY, logoutBuilder.postBinding().encoded())
|
||||
.param(GeneralConstants.SAML_REQUEST_KEY, binding.postBinding(logoutBuilder.buildDocument()).encoded())
|
||||
.param(GeneralConstants.RELAY_STATE, userSession.getId()).asStatus();
|
||||
boolean success = status >=200 && status < 400;
|
||||
if (!success) {
|
||||
@@ -178,7 +180,8 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||
} else {
|
||||
try {
|
||||
SAML2LogoutRequestBuilder logoutBuilder = buildLogoutRequest(userSession, uriInfo, realm, singleLogoutServiceUrl);
|
||||
return logoutBuilder.postBinding().request();
|
||||
JaxrsSAML2BindingBuilder binding = buildLogoutBinding(userSession, realm);
|
||||
return binding.postBinding(logoutBuilder.buildDocument()).request(singleLogoutServiceUrl);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -192,13 +195,19 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||
.issuer(getEntityId(uriInfo, realm))
|
||||
.sessionIndex(userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SESSION_INDEX))
|
||||
.userPrincipal(userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT), userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT_NAMEFORMAT))
|
||||
.destination(singleLogoutServiceUrl)
|
||||
.destination(singleLogoutServiceUrl);
|
||||
return logoutBuilder;
|
||||
}
|
||||
|
||||
private JaxrsSAML2BindingBuilder buildLogoutBinding(UserSessionModel userSession, RealmModel realm) {
|
||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
|
||||
.relayState(userSession.getId());
|
||||
if (getConfig().isWantAuthnRequestsSigned()) {
|
||||
logoutBuilder.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
|
||||
binding.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
|
||||
.signatureAlgorithm(getSignatureAlgorithm())
|
||||
.signDocument();
|
||||
}
|
||||
return logoutBuilder;
|
||||
return binding;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -220,7 +229,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||
|
||||
|
||||
String descriptor =
|
||||
"<EntityDescriptor entityID=\"" + getEntityId(uriInfo, realm) + "\">\n" +
|
||||
"<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"" + getEntityId(uriInfo, realm) + "\">\n" +
|
||||
" <SPSSODescriptor AuthnRequestsSigned=\"" + getConfig().isWantAuthnRequestsSigned() + "\"\n" +
|
||||
" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol http://schemas.xmlsoap.org/ws/2003/07/secext\">\n" +
|
||||
" <NameIDFormat>" + getConfig().getNameIDPolicyFormat() + "\n" +
|
||||
@@ -245,4 +254,18 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||
"</EntityDescriptor>\n";
|
||||
return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build();
|
||||
}
|
||||
|
||||
public SignatureAlgorithm getSignatureAlgorithm() {
|
||||
String alg = getConfig().getSignatureAlgorithm();
|
||||
if (alg != null) {
|
||||
SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(alg);
|
||||
if (algorithm != null) return algorithm;
|
||||
}
|
||||
return SignatureAlgorithm.RSA_SHA256;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderDataMarshaller getMarshaller() {
|
||||
return new SAMLDataMarshaller();
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,14 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
|
||||
getConfig().put("wantAuthnRequestsSigned", String.valueOf(wantAuthnRequestsSigned));
|
||||
}
|
||||
|
||||
public String getSignatureAlgorithm() {
|
||||
return getConfig().get("signatureAlgorithm");
|
||||
}
|
||||
|
||||
public void setSignatureAlgorithm(String signatureAlgorithm) {
|
||||
getConfig().put("signatureAlgorithm", signatureAlgorithm);
|
||||
}
|
||||
|
||||
public String getEncryptionPublicKey() {
|
||||
return getConfig().get("encryptionPublicKey");
|
||||
}
|
||||
|
||||
+69
-65
@@ -73,75 +73,79 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
|
||||
List<EntityDescriptorType.EDTChoiceType> choiceType = entityType.getChoiceType();
|
||||
|
||||
if (!choiceType.isEmpty()) {
|
||||
EntityDescriptorType.EDTChoiceType edtChoiceType = choiceType.get(0);
|
||||
List<EntityDescriptorType.EDTDescriptorChoiceType> descriptors = edtChoiceType.getDescriptors();
|
||||
IDPSSODescriptorType idpDescriptor = null;
|
||||
|
||||
if (!descriptors.isEmpty()) {
|
||||
EntityDescriptorType.EDTDescriptorChoiceType edtDescriptorChoiceType = descriptors.get(0);
|
||||
IDPSSODescriptorType idpDescriptor = edtDescriptorChoiceType.getIdpDescriptor();
|
||||
//Metadata documents can contain multiple Descriptors (See ADFS metadata documents) such as RoleDescriptor, SPSSODescriptor, IDPSSODescriptor.
|
||||
//So we need to loop through to find the IDPSSODescriptor.
|
||||
for(EntityDescriptorType.EDTChoiceType edtChoiceType : entityType.getChoiceType()) {
|
||||
List<EntityDescriptorType.EDTDescriptorChoiceType> descriptors = edtChoiceType.getDescriptors();
|
||||
|
||||
if (idpDescriptor != null) {
|
||||
SAMLIdentityProviderConfig samlIdentityProviderConfig = new SAMLIdentityProviderConfig();
|
||||
String singleSignOnServiceUrl = null;
|
||||
boolean postBinding = false;
|
||||
for (EndpointType endpoint : idpDescriptor.getSingleSignOnService()) {
|
||||
if (endpoint.getBinding().toString().equals(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get())) {
|
||||
singleSignOnServiceUrl = endpoint.getLocation().toString();
|
||||
postBinding = true;
|
||||
break;
|
||||
} else if (endpoint.getBinding().toString().equals(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get())){
|
||||
singleSignOnServiceUrl = endpoint.getLocation().toString();
|
||||
}
|
||||
}
|
||||
String singleLogoutServiceUrl = null;
|
||||
for (EndpointType endpoint : idpDescriptor.getSingleLogoutService()) {
|
||||
if (postBinding && endpoint.getBinding().toString().equals(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get())) {
|
||||
singleLogoutServiceUrl = endpoint.getLocation().toString();
|
||||
break;
|
||||
} else if (!postBinding && endpoint.getBinding().toString().equals(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get())){
|
||||
singleLogoutServiceUrl = endpoint.getLocation().toString();
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
samlIdentityProviderConfig.setSingleLogoutServiceUrl(singleLogoutServiceUrl);
|
||||
samlIdentityProviderConfig.setSingleSignOnServiceUrl(singleSignOnServiceUrl);
|
||||
samlIdentityProviderConfig.setWantAuthnRequestsSigned(idpDescriptor.isWantAuthnRequestsSigned());
|
||||
samlIdentityProviderConfig.setValidateSignature(idpDescriptor.isWantAuthnRequestsSigned());
|
||||
samlIdentityProviderConfig.setPostBindingResponse(postBinding);
|
||||
samlIdentityProviderConfig.setPostBindingAuthnRequest(postBinding);
|
||||
|
||||
List<KeyDescriptorType> keyDescriptor = idpDescriptor.getKeyDescriptor();
|
||||
String defaultCertificate = null;
|
||||
|
||||
if (keyDescriptor != null) {
|
||||
for (KeyDescriptorType keyDescriptorType : keyDescriptor) {
|
||||
Element keyInfo = keyDescriptorType.getKeyInfo();
|
||||
Element x509KeyInfo = DocumentUtil.getChildElement(keyInfo, new QName("dsig", "X509Certificate"));
|
||||
|
||||
if (KeyTypes.SIGNING.equals(keyDescriptorType.getUse())) {
|
||||
samlIdentityProviderConfig.setSigningCertificate(x509KeyInfo.getTextContent());
|
||||
} else if (KeyTypes.ENCRYPTION.equals(keyDescriptorType.getUse())) {
|
||||
samlIdentityProviderConfig.setEncryptionPublicKey(x509KeyInfo.getTextContent());
|
||||
} else if (keyDescriptorType.getUse() == null) {
|
||||
defaultCertificate = x509KeyInfo.getTextContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultCertificate != null) {
|
||||
if (samlIdentityProviderConfig.getSigningCertificate() == null) {
|
||||
samlIdentityProviderConfig.setSigningCertificate(defaultCertificate);
|
||||
}
|
||||
|
||||
if (samlIdentityProviderConfig.getEncryptionPublicKey() == null) {
|
||||
samlIdentityProviderConfig.setEncryptionPublicKey(defaultCertificate);
|
||||
}
|
||||
}
|
||||
|
||||
return samlIdentityProviderConfig.getConfig();
|
||||
if(!descriptors.isEmpty() && descriptors.get(0).getIdpDescriptor() != null) {
|
||||
idpDescriptor = descriptors.get(0).getIdpDescriptor();
|
||||
}
|
||||
}
|
||||
|
||||
if (idpDescriptor != null) {
|
||||
SAMLIdentityProviderConfig samlIdentityProviderConfig = new SAMLIdentityProviderConfig();
|
||||
String singleSignOnServiceUrl = null;
|
||||
boolean postBinding = false;
|
||||
for (EndpointType endpoint : idpDescriptor.getSingleSignOnService()) {
|
||||
if (endpoint.getBinding().toString().equals(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get())) {
|
||||
singleSignOnServiceUrl = endpoint.getLocation().toString();
|
||||
postBinding = true;
|
||||
break;
|
||||
} else if (endpoint.getBinding().toString().equals(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get())){
|
||||
singleSignOnServiceUrl = endpoint.getLocation().toString();
|
||||
}
|
||||
}
|
||||
String singleLogoutServiceUrl = null;
|
||||
for (EndpointType endpoint : idpDescriptor.getSingleLogoutService()) {
|
||||
if (postBinding && endpoint.getBinding().toString().equals(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get())) {
|
||||
singleLogoutServiceUrl = endpoint.getLocation().toString();
|
||||
break;
|
||||
} else if (!postBinding && endpoint.getBinding().toString().equals(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get())){
|
||||
singleLogoutServiceUrl = endpoint.getLocation().toString();
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
samlIdentityProviderConfig.setSingleLogoutServiceUrl(singleLogoutServiceUrl);
|
||||
samlIdentityProviderConfig.setSingleSignOnServiceUrl(singleSignOnServiceUrl);
|
||||
samlIdentityProviderConfig.setWantAuthnRequestsSigned(idpDescriptor.isWantAuthnRequestsSigned());
|
||||
samlIdentityProviderConfig.setValidateSignature(idpDescriptor.isWantAuthnRequestsSigned());
|
||||
samlIdentityProviderConfig.setPostBindingResponse(postBinding);
|
||||
samlIdentityProviderConfig.setPostBindingAuthnRequest(postBinding);
|
||||
|
||||
List<KeyDescriptorType> keyDescriptor = idpDescriptor.getKeyDescriptor();
|
||||
String defaultCertificate = null;
|
||||
|
||||
if (keyDescriptor != null) {
|
||||
for (KeyDescriptorType keyDescriptorType : keyDescriptor) {
|
||||
Element keyInfo = keyDescriptorType.getKeyInfo();
|
||||
Element x509KeyInfo = DocumentUtil.getChildElement(keyInfo, new QName("dsig", "X509Certificate"));
|
||||
|
||||
if (KeyTypes.SIGNING.equals(keyDescriptorType.getUse())) {
|
||||
samlIdentityProviderConfig.setSigningCertificate(x509KeyInfo.getTextContent());
|
||||
} else if (KeyTypes.ENCRYPTION.equals(keyDescriptorType.getUse())) {
|
||||
samlIdentityProviderConfig.setEncryptionPublicKey(x509KeyInfo.getTextContent());
|
||||
} else if (keyDescriptorType.getUse() == null) {
|
||||
defaultCertificate = x509KeyInfo.getTextContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultCertificate != null) {
|
||||
if (samlIdentityProviderConfig.getSigningCertificate() == null) {
|
||||
samlIdentityProviderConfig.setSigningCertificate(defaultCertificate);
|
||||
}
|
||||
|
||||
if (samlIdentityProviderConfig.getEncryptionPublicKey() == null) {
|
||||
samlIdentityProviderConfig.setEncryptionPublicKey(defaultCertificate);
|
||||
}
|
||||
}
|
||||
|
||||
return samlIdentityProviderConfig.getConfig();
|
||||
}
|
||||
}
|
||||
} catch (ParsingException pe) {
|
||||
throw new RuntimeException("Could not parse IdP SAML Metadata", pe);
|
||||
|
||||
+141
-140
@@ -1,140 +1,141 @@
|
||||
package org.keycloak.broker.saml.mappers;
|
||||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.HardcodedRoleMapper;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.broker.saml.SAMLEndpoint;
|
||||
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class AttributeToRoleMapper extends AbstractIdentityProviderMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
public static final String ATTRIBUTE_NAME = "attribute.name";
|
||||
public static final String ATTRIBUTE_FRIENDLY_NAME = "attribute.friendly.name";
|
||||
public static final String ATTRIBUTE_VALUE = "attribute.value";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_NAME);
|
||||
property.setLabel("Attribute Name");
|
||||
property.setHelpText("Name of attribute to search for in assertion. You can leave this blank and specify a friendly name instead.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_FRIENDLY_NAME);
|
||||
property.setLabel("Friendly Name");
|
||||
property.setHelpText("Friendly name of attribute to search for in assertion. You can leave this blank and specify a name instead.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_VALUE);
|
||||
property.setLabel("Attribute Value");
|
||||
property.setHelpText("Value the attribute must have. If the attribute is a list, then the value must be contained in the list.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(HardcodedRoleMapper.ROLE);
|
||||
property.setLabel("Role");
|
||||
property.setHelpText("Role to grant to user. To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "saml-role-idp-mapper";
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Role Mapper";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "SAML Attribute to Role";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (isAttributePresent(mapperModel, context)) {
|
||||
RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.grantRole(role);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isAttributePresent(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String name = mapperModel.getConfig().get(ATTRIBUTE_NAME);
|
||||
if (name != null && name.trim().equals("")) name = null;
|
||||
String friendly = mapperModel.getConfig().get(ATTRIBUTE_FRIENDLY_NAME);
|
||||
if (friendly != null && friendly.trim().equals("")) friendly = null;
|
||||
String desiredValue = mapperModel.getConfig().get(ATTRIBUTE_VALUE);
|
||||
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
|
||||
for (AttributeStatementType statement : assertion.getAttributeStatements()) {
|
||||
for (AttributeStatementType.ASTChoiceType choice : statement.getAttributes()) {
|
||||
AttributeType attr = choice.getAttribute();
|
||||
if (name != null && !name.equals(attr.getName())) continue;
|
||||
if (friendly != null && !name.equals(attr.getFriendlyName())) continue;
|
||||
for (Object val : attr.getAttributeValue()) {
|
||||
if (val.equals(desiredValue)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (!isAttributePresent(mapperModel, context)) {
|
||||
RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.deleteRoleMapping(role);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "If a claim exists, grant the user the specified realm or application role.";
|
||||
}
|
||||
|
||||
}
|
||||
package org.keycloak.broker.saml.mappers;
|
||||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.HardcodedRoleMapper;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.broker.saml.SAMLEndpoint;
|
||||
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class AttributeToRoleMapper extends AbstractIdentityProviderMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
public static final String ATTRIBUTE_NAME = "attribute.name";
|
||||
public static final String ATTRIBUTE_FRIENDLY_NAME = "attribute.friendly.name";
|
||||
public static final String ATTRIBUTE_VALUE = "attribute.value";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_NAME);
|
||||
property.setLabel("Attribute Name");
|
||||
property.setHelpText("Name of attribute to search for in assertion. You can leave this blank and specify a friendly name instead.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_FRIENDLY_NAME);
|
||||
property.setLabel("Friendly Name");
|
||||
property.setHelpText("Friendly name of attribute to search for in assertion. You can leave this blank and specify a name instead.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_VALUE);
|
||||
property.setLabel("Attribute Value");
|
||||
property.setHelpText("Value the attribute must have. If the attribute is a list, then the value must be contained in the list.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(HardcodedRoleMapper.ROLE);
|
||||
property.setLabel("Role");
|
||||
property.setHelpText("Role to grant to user. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
|
||||
property.setType(ProviderConfigProperty.ROLE_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "saml-role-idp-mapper";
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Role Mapper";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "SAML Attribute to Role";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (isAttributePresent(mapperModel, context)) {
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.grantRole(role);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isAttributePresent(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String name = mapperModel.getConfig().get(ATTRIBUTE_NAME);
|
||||
if (name != null && name.trim().equals("")) name = null;
|
||||
String friendly = mapperModel.getConfig().get(ATTRIBUTE_FRIENDLY_NAME);
|
||||
if (friendly != null && friendly.trim().equals("")) friendly = null;
|
||||
String desiredValue = mapperModel.getConfig().get(ATTRIBUTE_VALUE);
|
||||
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
|
||||
for (AttributeStatementType statement : assertion.getAttributeStatements()) {
|
||||
for (AttributeStatementType.ASTChoiceType choice : statement.getAttributes()) {
|
||||
AttributeType attr = choice.getAttribute();
|
||||
if (name != null && !name.equals(attr.getName())) continue;
|
||||
if (friendly != null && !name.equals(attr.getFriendlyName())) continue;
|
||||
for (Object val : attr.getAttributeValue()) {
|
||||
if (val.equals(desiredValue)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (!isAttributePresent(mapperModel, context)) {
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.deleteRoleMapping(role);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "If a claim exists, grant the user the specified realm or application role.";
|
||||
}
|
||||
|
||||
}
|
||||
+129
-132
@@ -1,132 +1,129 @@
|
||||
package org.keycloak.broker.saml.mappers;
|
||||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.broker.saml.SAMLEndpoint;
|
||||
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UserAttributeMapper extends AbstractIdentityProviderMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
public static final String ATTRIBUTE_NAME = "attribute.name";
|
||||
public static final String ATTRIBUTE_FRIENDLY_NAME = "attribute.friendly.name";
|
||||
public static final String USER_ATTRIBUTE = "user.attribute";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_NAME);
|
||||
property.setLabel("Attribute Name");
|
||||
property.setHelpText("Name of attribute to search for in assertion. You can leave this blank and specify a friendly name instead.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_FRIENDLY_NAME);
|
||||
property.setLabel("Friendly Name");
|
||||
property.setHelpText("Friendly name of attribute to search for in assertion. You can leave this blank and specify a name instead.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(USER_ATTRIBUTE);
|
||||
property.setLabel("User Attribute Name");
|
||||
property.setHelpText("User attribute name to store saml attribute.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "saml-user-attribute-idp-mapper";
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
|
||||
Object value = getAttribute(mapperModel, context);
|
||||
if (value != null) {
|
||||
user.setAttribute(attribute, value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
protected String getAttribute(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String name = mapperModel.getConfig().get(ATTRIBUTE_NAME);
|
||||
if (name != null && name.trim().equals("")) name = null;
|
||||
String friendly = mapperModel.getConfig().get(ATTRIBUTE_FRIENDLY_NAME);
|
||||
if (friendly != null && friendly.trim().equals("")) friendly = null;
|
||||
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
|
||||
for (AttributeStatementType statement : assertion.getAttributeStatements()) {
|
||||
for (AttributeStatementType.ASTChoiceType choice : statement.getAttributes()) {
|
||||
AttributeType attr = choice.getAttribute();
|
||||
if (name != null && !name.equals(attr.getName())) continue;
|
||||
if (friendly != null && !name.equals(attr.getFriendlyName())) continue;
|
||||
|
||||
List<Object> attributeValue = attr.getAttributeValue();
|
||||
if (attributeValue == null || attributeValue.isEmpty()) return null;
|
||||
return attributeValue.get(0).toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
|
||||
Object value = getAttribute(mapperModel, context);
|
||||
String current = user.getAttribute(attribute);
|
||||
if (value != null && !value.equals(current)) {
|
||||
user.setAttribute(attribute, value.toString());
|
||||
} else if (value == null) {
|
||||
user.removeAttribute(attribute);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Import declared saml attribute if it exists in assertion into the specified user attribute.";
|
||||
}
|
||||
|
||||
}
|
||||
package org.keycloak.broker.saml.mappers;
|
||||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.saml.SAMLEndpoint;
|
||||
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UserAttributeMapper extends AbstractIdentityProviderMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
public static final String ATTRIBUTE_NAME = "attribute.name";
|
||||
public static final String ATTRIBUTE_FRIENDLY_NAME = "attribute.friendly.name";
|
||||
public static final String USER_ATTRIBUTE = "user.attribute";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_NAME);
|
||||
property.setLabel("Attribute Name");
|
||||
property.setHelpText("Name of attribute to search for in assertion. You can leave this blank and specify a friendly name instead.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_FRIENDLY_NAME);
|
||||
property.setLabel("Friendly Name");
|
||||
property.setHelpText("Friendly name of attribute to search for in assertion. You can leave this blank and specify a name instead.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(USER_ATTRIBUTE);
|
||||
property.setLabel("User Attribute Name");
|
||||
property.setHelpText("User attribute name to store saml attribute.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "saml-user-attribute-idp-mapper";
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
|
||||
Object value = getAttribute(mapperModel, context);
|
||||
if (value != null) {
|
||||
context.setUserAttribute(attribute, value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
protected String getAttribute(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String name = mapperModel.getConfig().get(ATTRIBUTE_NAME);
|
||||
if (name != null && name.trim().equals("")) name = null;
|
||||
String friendly = mapperModel.getConfig().get(ATTRIBUTE_FRIENDLY_NAME);
|
||||
if (friendly != null && friendly.trim().equals("")) friendly = null;
|
||||
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
|
||||
for (AttributeStatementType statement : assertion.getAttributeStatements()) {
|
||||
for (AttributeStatementType.ASTChoiceType choice : statement.getAttributes()) {
|
||||
AttributeType attr = choice.getAttribute();
|
||||
if (name != null && !name.equals(attr.getName())) continue;
|
||||
if (friendly != null && !friendly.equals(attr.getFriendlyName())) continue;
|
||||
|
||||
List<Object> attributeValue = attr.getAttributeValue();
|
||||
if (attributeValue == null || attributeValue.isEmpty()) return null;
|
||||
return attributeValue.get(0).toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
|
||||
Object value = getAttribute(mapperModel, context);
|
||||
String current = user.getFirstAttribute(attribute);
|
||||
if (value != null && !value.equals(current)) {
|
||||
user.setSingleAttribute(attribute, value.toString());
|
||||
} else if (value == null) {
|
||||
user.removeAttribute(attribute);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Import declared saml attribute if it exists in assertion into the specified user attribute.";
|
||||
}
|
||||
|
||||
}
|
||||
+128
-133
@@ -1,133 +1,128 @@
|
||||
package org.keycloak.broker.saml.mappers;
|
||||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.saml.SAMLEndpoint;
|
||||
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.assertion.SubjectType;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UsernameTemplateMapper extends AbstractIdentityProviderMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
public static final String TEMPLATE = "template";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(TEMPLATE);
|
||||
property.setLabel("Template");
|
||||
property.setHelpText("Template to use to format the username to import. Substitutions are enclosed in ${}. For example: '${ALIAS}.${NAMEID}'. ALIAS is the provider alias. NAMEID is that SAML name id assertion. ATTRIBUTE.<NAME> references a SAML attribute where name is the attribute name or friendly name.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
property.setDefaultValue("${ALIAS}.${NAMEID}");
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "saml-username-idp-mapper";
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Preprocessor";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Username Template Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
|
||||
String template = mapperModel.getConfig().get(TEMPLATE);
|
||||
Matcher m = substitution.matcher(template);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
while (m.find()) {
|
||||
String variable = m.group(1);
|
||||
if (variable.equals("ALIAS")) {
|
||||
m.appendReplacement(sb, context.getIdpConfig().getAlias());
|
||||
} else if (variable.equals("UUID")) {
|
||||
m.appendReplacement(sb, KeycloakModelUtils.generateId());
|
||||
} else if (variable.equals("NAMEID")) {
|
||||
SubjectType subject = assertion.getSubject();
|
||||
SubjectType.STSubType subType = subject.getSubType();
|
||||
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
|
||||
m.appendReplacement(sb, subjectNameID.getValue());
|
||||
} else if (variable.startsWith("ATTRIBUTE.")) {
|
||||
String name = variable.substring("ATTRIBUTE.".length());
|
||||
String value = "";
|
||||
for (AttributeStatementType statement : assertion.getAttributeStatements()) {
|
||||
for (AttributeStatementType.ASTChoiceType choice : statement.getAttributes()) {
|
||||
AttributeType attr = choice.getAttribute();
|
||||
if (name.equals(attr.getName()) || name.equals(attr.getFriendlyName())) {
|
||||
List<Object> attributeValue = attr.getAttributeValue();
|
||||
if (attributeValue != null && !attributeValue.isEmpty()) {
|
||||
value = attributeValue.get(0).toString();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
m.appendReplacement(sb, value);
|
||||
} else {
|
||||
m.appendReplacement(sb, m.group(1));
|
||||
}
|
||||
|
||||
}
|
||||
m.appendTail(sb);
|
||||
context.setModelUsername(sb.toString());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Format the username to import.";
|
||||
}
|
||||
|
||||
}
|
||||
package org.keycloak.broker.saml.mappers;
|
||||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.saml.SAMLEndpoint;
|
||||
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.assertion.SubjectType;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UsernameTemplateMapper extends AbstractIdentityProviderMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
public static final String TEMPLATE = "template";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(TEMPLATE);
|
||||
property.setLabel("Template");
|
||||
property.setHelpText("Template to use to format the username to import. Substitutions are enclosed in ${}. For example: '${ALIAS}.${NAMEID}'. ALIAS is the provider alias. NAMEID is that SAML name id assertion. ATTRIBUTE.<NAME> references a SAML attribute where name is the attribute name or friendly name.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
property.setDefaultValue("${ALIAS}.${NAMEID}");
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "saml-username-idp-mapper";
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Preprocessor";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Username Template Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
|
||||
String template = mapperModel.getConfig().get(TEMPLATE);
|
||||
Matcher m = substitution.matcher(template);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
while (m.find()) {
|
||||
String variable = m.group(1);
|
||||
if (variable.equals("ALIAS")) {
|
||||
m.appendReplacement(sb, context.getIdpConfig().getAlias());
|
||||
} else if (variable.equals("UUID")) {
|
||||
m.appendReplacement(sb, KeycloakModelUtils.generateId());
|
||||
} else if (variable.equals("NAMEID")) {
|
||||
SubjectType subject = assertion.getSubject();
|
||||
SubjectType.STSubType subType = subject.getSubType();
|
||||
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
|
||||
m.appendReplacement(sb, subjectNameID.getValue());
|
||||
} else if (variable.startsWith("ATTRIBUTE.")) {
|
||||
String name = variable.substring("ATTRIBUTE.".length());
|
||||
String value = "";
|
||||
for (AttributeStatementType statement : assertion.getAttributeStatements()) {
|
||||
for (AttributeStatementType.ASTChoiceType choice : statement.getAttributes()) {
|
||||
AttributeType attr = choice.getAttribute();
|
||||
if (name.equals(attr.getName()) || name.equals(attr.getFriendlyName())) {
|
||||
List<Object> attributeValue = attr.getAttributeValue();
|
||||
if (attributeValue != null && !attributeValue.isEmpty()) {
|
||||
value = attributeValue.get(0).toString();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
m.appendReplacement(sb, value);
|
||||
} else {
|
||||
m.appendReplacement(sb, m.group(1));
|
||||
}
|
||||
|
||||
}
|
||||
m.appendTail(sb);
|
||||
context.setModelUsername(sb.toString());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Format the username to import.";
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
org.keycloak.broker.saml.mappers.AttributeToRoleMapper
|
||||
org.keycloak.broker.saml.mappers.UserAttributeMapper
|
||||
org.keycloak.broker.saml.mappers.AttributeToRoleMapper
|
||||
org.keycloak.broker.saml.mappers.UserAttributeMapper
|
||||
org.keycloak.broker.saml.mappers.UsernameTemplateMapper
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.keycloak.broker.saml;
|
||||
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class SAMLDataMarshallerTest {
|
||||
|
||||
private static final String TEST_RESPONSE = "<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_4804cf50-cd96-4b92-823e-89adaa0c78ba\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.920Z\" Destination=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\" InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"></samlp:StatusCode></samlp:Status><saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.911Z\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><saml:Subject><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\">test-user</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\" NotOnOrAfter=\"2015-11-06T11:05:31.911Z\" Recipient=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\"></saml:SubjectConfirmationData></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"2015-11-06T11:00:31.911Z\" NotOnOrAfter=\"2015-11-06T11:01:31.911Z\"><saml:AudienceRestriction><saml:Audience>http://localhost:8081/auth/realms/realm-with-broker</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name=\"mobile\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">617-666-7777</saml:AttributeValue></saml:Attribute><saml:Attribute Name=\"urn:oid:1.2.840.113549.1.9.1\" FriendlyName=\"email\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">test-user@localhost</saml:AttributeValue></saml:Attribute></saml:AttributeStatement><saml:AttributeStatement><saml:Attribute Name=\"Role\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">manager</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>";
|
||||
|
||||
private static final String TEST_ASSERTION = "<saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.911Z\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><saml:Subject><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\">test-user</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\" NotOnOrAfter=\"2015-11-06T11:05:31.911Z\" Recipient=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\"></saml:SubjectConfirmationData></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"2015-11-06T11:00:31.911Z\" NotOnOrAfter=\"2015-11-06T11:01:31.911Z\"><saml:AudienceRestriction><saml:Audience>http://localhost:8081/auth/realms/realm-with-broker</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name=\"mobile\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">617-666-7777</saml:AttributeValue></saml:Attribute><saml:Attribute Name=\"urn:oid:1.2.840.113549.1.9.1\" FriendlyName=\"email\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">test-user@localhost</saml:AttributeValue></saml:Attribute></saml:AttributeStatement><saml:AttributeStatement><saml:Attribute Name=\"Role\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">manager</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion>";
|
||||
|
||||
private static final String TEST_AUTHN_TYPE = "<saml:AuthnStatement xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement>";
|
||||
|
||||
@Test
|
||||
public void testParseResponse() throws Exception {
|
||||
SAMLDataMarshaller serializer = new SAMLDataMarshaller();
|
||||
ResponseType responseType = serializer.deserialize(TEST_RESPONSE, ResponseType.class);
|
||||
|
||||
// test ResponseType
|
||||
Assert.assertEquals(responseType.getID(), "ID_4804cf50-cd96-4b92-823e-89adaa0c78ba");
|
||||
Assert.assertEquals(responseType.getDestination(), "http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint");
|
||||
Assert.assertEquals(responseType.getIssuer().getValue(), "http://localhost:8082/auth/realms/realm-with-saml-idp-basic");
|
||||
Assert.assertEquals(responseType.getAssertions().get(0).getID(), "ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9");
|
||||
|
||||
// back to String
|
||||
String serialized = serializer.serialize(responseType);
|
||||
Assert.assertEquals(TEST_RESPONSE, serialized);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseAssertion() throws Exception {
|
||||
SAMLDataMarshaller serializer = new SAMLDataMarshaller();
|
||||
AssertionType assertion = serializer.deserialize(TEST_ASSERTION, AssertionType.class);
|
||||
|
||||
// test assertion
|
||||
Assert.assertEquals(assertion.getID(), "ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9");
|
||||
Assert.assertEquals(((NameIDType) assertion.getSubject().getSubType().getBaseID()).getValue(), "test-user");
|
||||
|
||||
// back to String
|
||||
String serialized = serializer.serialize(assertion);
|
||||
Assert.assertEquals(TEST_ASSERTION, serialized);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseAuthnType() throws Exception {
|
||||
SAMLDataMarshaller serializer = new SAMLDataMarshaller();
|
||||
AuthnStatementType authnStatement = serializer.deserialize(TEST_AUTHN_TYPE, AuthnStatementType.class);
|
||||
|
||||
// test authnStatement
|
||||
Assert.assertEquals(authnStatement.getSessionIndex(), "fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5");
|
||||
|
||||
// back to String
|
||||
String serialized = serializer.serialize(authnStatement);
|
||||
Assert.assertEquals(TEST_AUTHN_TYPE, serialized);
|
||||
}
|
||||
}
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-client-registration-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.8.0.CR3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-client-registration-api</artifactId>
|
||||
<name>Keycloak Client Registration API</name>
|
||||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.keycloak.client.registration;
|
||||
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.HttpRequest;
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public abstract class Auth {
|
||||
|
||||
public abstract void addAuth(HttpRequest request);
|
||||
|
||||
public static Auth token(String token) {
|
||||
return new BearerTokenAuth(token);
|
||||
}
|
||||
|
||||
public static Auth token(ClientInitialAccessPresentation initialAccess) {
|
||||
return new BearerTokenAuth(initialAccess.getToken());
|
||||
}
|
||||
|
||||
public static Auth token(ClientRepresentation client) {
|
||||
return new BearerTokenAuth(client.getRegistrationAccessToken());
|
||||
}
|
||||
|
||||
public static Auth token(OIDCClientRepresentation client) {
|
||||
return new BearerTokenAuth(client.getRegistrationAccessToken());
|
||||
}
|
||||
|
||||
public static Auth client(String clientId, String clientSecret) {
|
||||
return new BasicAuth(clientId, clientSecret);
|
||||
}
|
||||
|
||||
private static class BearerTokenAuth extends Auth {
|
||||
|
||||
private String token;
|
||||
|
||||
public BearerTokenAuth(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAuth(HttpRequest request) {
|
||||
request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
}
|
||||
}
|
||||
|
||||
private static class BasicAuth extends Auth {
|
||||
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
public BasicAuth(String username, String password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAuth(HttpRequest request) {
|
||||
String val = Base64.encodeBytes((username + ":" + password).getBytes());
|
||||
request.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + val);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
package org.keycloak.client.registration;
|
||||
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.codehaus.jackson.map.annotate.JsonSerialize;
|
||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class ClientRegistration {
|
||||
|
||||
public static final ObjectMapper outputMapper = new ObjectMapper();
|
||||
static {
|
||||
outputMapper.getSerializationConfig().addMixInAnnotations(ClientRepresentation.class, ClientRepresentationMixIn.class);
|
||||
outputMapper.getSerializationConfig().addMixInAnnotations(OIDCClientRepresentation.class, OIDCClientRepresentationMixIn.class);
|
||||
outputMapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
|
||||
}
|
||||
|
||||
private final String JSON = "application/json";
|
||||
private final String XML = "application/xml";
|
||||
|
||||
private final String DEFAULT = "default";
|
||||
private final String INSTALLATION = "install";
|
||||
private final String OIDC = "openid-connect";
|
||||
private final String SAML = "saml2-entity-descriptor";
|
||||
|
||||
private HttpUtil httpUtil;
|
||||
|
||||
public static ClientRegistrationBuilder create() {
|
||||
return new ClientRegistrationBuilder();
|
||||
}
|
||||
|
||||
ClientRegistration(HttpUtil httpUtil) {
|
||||
this.httpUtil = httpUtil;
|
||||
}
|
||||
|
||||
public void close() throws ClientRegistrationException {
|
||||
if (httpUtil != null) {
|
||||
httpUtil.close();
|
||||
}
|
||||
httpUtil = null;
|
||||
}
|
||||
|
||||
public ClientRegistration auth(Auth auth) {
|
||||
httpUtil.setAuth(auth);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientRepresentation create(ClientRepresentation client) throws ClientRegistrationException {
|
||||
String content = serialize(client);
|
||||
InputStream resultStream = httpUtil.doPost(content, JSON, JSON, DEFAULT);
|
||||
return deserialize(resultStream, ClientRepresentation.class);
|
||||
}
|
||||
|
||||
public ClientRepresentation get(String clientId) throws ClientRegistrationException {
|
||||
InputStream resultStream = httpUtil.doGet(JSON, DEFAULT, clientId);
|
||||
return resultStream != null ? deserialize(resultStream, ClientRepresentation.class) : null;
|
||||
}
|
||||
|
||||
public AdapterConfig getAdapterConfig(String clientId) throws ClientRegistrationException {
|
||||
InputStream resultStream = httpUtil.doGet(JSON, INSTALLATION, clientId);
|
||||
return resultStream != null ? deserialize(resultStream, AdapterConfig.class) : null;
|
||||
}
|
||||
|
||||
public ClientRepresentation update(ClientRepresentation client) throws ClientRegistrationException {
|
||||
String content = serialize(client);
|
||||
InputStream resultStream = httpUtil.doPut(content, JSON, JSON, DEFAULT, client.getClientId());
|
||||
return resultStream != null ? deserialize(resultStream, ClientRepresentation.class) : null;
|
||||
}
|
||||
|
||||
public void delete(ClientRepresentation client) throws ClientRegistrationException {
|
||||
delete(client.getClientId());
|
||||
}
|
||||
|
||||
public void delete(String clientId) throws ClientRegistrationException {
|
||||
httpUtil.doDelete(DEFAULT, clientId);
|
||||
}
|
||||
|
||||
public OIDCClientRegistration oidc() {
|
||||
return new OIDCClientRegistration();
|
||||
}
|
||||
|
||||
public SAMLClientRegistration saml() {
|
||||
return new SAMLClientRegistration();
|
||||
}
|
||||
|
||||
public static String serialize(Object obj) throws ClientRegistrationException {
|
||||
try {
|
||||
return outputMapper.writeValueAsString(obj);
|
||||
} catch (IOException e) {
|
||||
throw new ClientRegistrationException("Failed to write json object", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T deserialize(InputStream inputStream, Class<T> clazz) throws ClientRegistrationException {
|
||||
try {
|
||||
return JsonSerialization.readValue(inputStream, clazz);
|
||||
} catch (IOException e) {
|
||||
throw new ClientRegistrationException("Failed to read json object", e);
|
||||
}
|
||||
}
|
||||
|
||||
public class OIDCClientRegistration {
|
||||
|
||||
public OIDCClientRepresentation create(OIDCClientRepresentation client) throws ClientRegistrationException {
|
||||
String content = serialize(client);
|
||||
InputStream resultStream = httpUtil.doPost(content, JSON, JSON, OIDC);
|
||||
return deserialize(resultStream, OIDCClientRepresentation.class);
|
||||
}
|
||||
|
||||
public OIDCClientRepresentation get(String clientId) throws ClientRegistrationException {
|
||||
InputStream resultStream = httpUtil.doGet(JSON, OIDC, clientId);
|
||||
return resultStream != null ? deserialize(resultStream, OIDCClientRepresentation.class) : null;
|
||||
}
|
||||
|
||||
public OIDCClientRepresentation update(OIDCClientRepresentation client) throws ClientRegistrationException {
|
||||
String content = serialize(client);
|
||||
InputStream resultStream = httpUtil.doPut(content, JSON, JSON, OIDC, client.getClientId());
|
||||
return resultStream != null ? deserialize(resultStream, OIDCClientRepresentation.class) : null;
|
||||
}
|
||||
|
||||
public void delete(OIDCClientRepresentation client) throws ClientRegistrationException {
|
||||
delete(client.getClientId());
|
||||
}
|
||||
|
||||
public void delete(String clientId) throws ClientRegistrationException {
|
||||
httpUtil.doDelete(OIDC, clientId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class SAMLClientRegistration {
|
||||
|
||||
public ClientRepresentation create(String entityDescriptor) throws ClientRegistrationException {
|
||||
InputStream resultStream = httpUtil.doPost(entityDescriptor, XML, JSON, SAML);
|
||||
return deserialize(resultStream, ClientRepresentation.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class ClientRegistrationBuilder {
|
||||
|
||||
private String url;
|
||||
private HttpClient httpClient;
|
||||
|
||||
ClientRegistrationBuilder() {
|
||||
}
|
||||
|
||||
public ClientRegistrationBuilder url(String realmUrl) {
|
||||
url = realmUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientRegistrationBuilder url(String authUrl, String realm) {
|
||||
url = HttpUtil.getUrl(authUrl, "realms", realm, "clients");
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientRegistrationBuilder httpClient(HttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientRegistration build() {
|
||||
if (url == null) {
|
||||
throw new IllegalStateException("url not configured");
|
||||
}
|
||||
|
||||
if (httpClient == null) {
|
||||
httpClient = HttpClients.createDefault();
|
||||
}
|
||||
|
||||
return new ClientRegistration(new HttpUtil(httpClient, url));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package org.keycloak.client.registration;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class ClientRegistrationException extends Exception {
|
||||
|
||||
public ClientRegistrationException(String s, Throwable throwable) {
|
||||
super(s, throwable);
|
||||
}
|
||||
|
||||
public ClientRegistrationException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package org.keycloak.client.registration;
|
||||
|
||||
import org.codehaus.jackson.annotate.JsonIgnore;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
abstract class ClientRepresentationMixIn {
|
||||
|
||||
@JsonIgnore
|
||||
String registrationAccessToken;
|
||||
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package org.keycloak.client.registration;
|
||||
|
||||
import org.apache.http.StatusLine;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class HttpErrorException extends IOException {
|
||||
|
||||
private StatusLine statusLine;
|
||||
|
||||
public HttpErrorException(StatusLine statusLine) {
|
||||
this.statusLine = statusLine;
|
||||
}
|
||||
|
||||
public StatusLine getStatusLine() {
|
||||
return statusLine;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package org.keycloak.client.registration;
|
||||
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.*;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.keycloak.client.registration.Auth;
|
||||
import org.keycloak.client.registration.ClientRegistrationException;
|
||||
import org.keycloak.client.registration.HttpErrorException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
class HttpUtil {
|
||||
|
||||
private HttpClient httpClient;
|
||||
|
||||
private String baseUri;
|
||||
|
||||
private Auth auth;
|
||||
|
||||
HttpUtil(HttpClient httpClient, String baseUri) {
|
||||
this.httpClient = httpClient;
|
||||
this.baseUri = baseUri;
|
||||
}
|
||||
|
||||
void setAuth(Auth auth) {
|
||||
this.auth = auth;
|
||||
}
|
||||
|
||||
InputStream doPost(String content, String contentType, String acceptType, String... path) throws ClientRegistrationException {
|
||||
try {
|
||||
HttpPost request = new HttpPost(getUrl(baseUri, path));
|
||||
|
||||
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
|
||||
request.setHeader(HttpHeaders.ACCEPT, acceptType);
|
||||
request.setEntity(new StringEntity(content));
|
||||
|
||||
addAuth(request);
|
||||
|
||||
HttpResponse response = httpClient.execute(request);
|
||||
InputStream responseStream = null;
|
||||
if (response.getEntity() != null) {
|
||||
responseStream = response.getEntity().getContent();
|
||||
}
|
||||
|
||||
if (response.getStatusLine().getStatusCode() == 201) {
|
||||
return responseStream;
|
||||
} else {
|
||||
responseStream.close();
|
||||
throw new HttpErrorException(response.getStatusLine());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ClientRegistrationException("Failed to send request", e);
|
||||
}
|
||||
}
|
||||
|
||||
InputStream doGet(String acceptType, String... path) throws ClientRegistrationException {
|
||||
try {
|
||||
HttpGet request = new HttpGet(getUrl(baseUri, path));
|
||||
|
||||
request.setHeader(HttpHeaders.ACCEPT, acceptType);
|
||||
|
||||
addAuth(request);
|
||||
|
||||
HttpResponse response = httpClient.execute(request);
|
||||
InputStream responseStream = null;
|
||||
if (response.getEntity() != null) {
|
||||
responseStream = response.getEntity().getContent();
|
||||
}
|
||||
|
||||
if (response.getStatusLine().getStatusCode() == 200) {
|
||||
return responseStream;
|
||||
} else if (response.getStatusLine().getStatusCode() == 404) {
|
||||
responseStream.close();
|
||||
return null;
|
||||
} else {
|
||||
if (responseStream != null) {
|
||||
responseStream.close();
|
||||
}
|
||||
throw new HttpErrorException(response.getStatusLine());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ClientRegistrationException("Failed to send request", e);
|
||||
}
|
||||
}
|
||||
|
||||
InputStream doPut(String content, String contentType, String acceptType, String... path) throws ClientRegistrationException {
|
||||
try {
|
||||
HttpPut request = new HttpPut(getUrl(baseUri, path));
|
||||
|
||||
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
|
||||
request.setHeader(HttpHeaders.ACCEPT, acceptType);
|
||||
request.setEntity(new StringEntity(content));
|
||||
|
||||
addAuth(request);
|
||||
|
||||
HttpResponse response = httpClient.execute(request);
|
||||
if (response.getEntity() != null) {
|
||||
response.getEntity().getContent();
|
||||
}
|
||||
|
||||
InputStream responseStream = null;
|
||||
if (response.getEntity() != null) {
|
||||
responseStream = response.getEntity().getContent();
|
||||
}
|
||||
|
||||
if (response.getStatusLine().getStatusCode() == 200) {
|
||||
return responseStream;
|
||||
} else {
|
||||
if (responseStream != null) {
|
||||
responseStream.close();
|
||||
}
|
||||
throw new HttpErrorException(response.getStatusLine());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ClientRegistrationException("Failed to send request", e);
|
||||
}
|
||||
}
|
||||
|
||||
void doDelete(String... path) throws ClientRegistrationException {
|
||||
try {
|
||||
HttpDelete request = new HttpDelete(getUrl(baseUri, path));
|
||||
|
||||
addAuth(request);
|
||||
|
||||
HttpResponse response = httpClient.execute(request);
|
||||
if (response.getEntity() != null) {
|
||||
response.getEntity().getContent().close();
|
||||
}
|
||||
|
||||
if (response.getStatusLine().getStatusCode() != 204) {
|
||||
throw new HttpErrorException(response.getStatusLine());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ClientRegistrationException("Failed to send request", e);
|
||||
}
|
||||
}
|
||||
|
||||
void close() throws ClientRegistrationException {
|
||||
if (httpClient instanceof CloseableHttpClient) {
|
||||
try {
|
||||
((CloseableHttpClient) httpClient).close();
|
||||
} catch (IOException e) {
|
||||
throw new ClientRegistrationException("Failed to close http client", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String getUrl(String baseUri, String... path) {
|
||||
StringBuilder s = new StringBuilder();
|
||||
s.append(baseUri);
|
||||
for (String p : path) {
|
||||
s.append('/');
|
||||
s.append(p);
|
||||
}
|
||||
return s.toString();
|
||||
}
|
||||
|
||||
private void addAuth(HttpRequestBase request) {
|
||||
if (auth != null) {
|
||||
auth.addAuth(request);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package org.keycloak.client.registration;
|
||||
|
||||
import org.codehaus.jackson.annotate.JsonIgnore;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
abstract class OIDCClientRepresentationMixIn {
|
||||
|
||||
@JsonIgnore
|
||||
private Integer client_id_issued_at;
|
||||
|
||||
@JsonIgnore
|
||||
private Integer client_secret_expires_at;
|
||||
|
||||
@JsonIgnore
|
||||
private String registration_client_uri;
|
||||
|
||||
@JsonIgnore
|
||||
private String registration_access_token;
|
||||
|
||||
}
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-client-registration-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.7.0.Final-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-client-registration-cli</artifactId>
|
||||
<name>Keycloak Client Registration CLI</name>
|
||||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-client-registration-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.aesh</groupId>
|
||||
<artifactId>aesh</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package org.keycloak.client.registration.cli;
|
||||
|
||||
import org.jboss.aesh.cl.parser.CommandLineParserException;
|
||||
import org.jboss.aesh.console.AeshConsole;
|
||||
import org.jboss.aesh.console.AeshConsoleBuilder;
|
||||
import org.jboss.aesh.console.Prompt;
|
||||
import org.jboss.aesh.console.command.Command;
|
||||
import org.jboss.aesh.console.command.CommandNotFoundException;
|
||||
import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder;
|
||||
import org.jboss.aesh.console.settings.Settings;
|
||||
import org.jboss.aesh.console.settings.SettingsBuilder;
|
||||
import org.jboss.aesh.terminal.Color;
|
||||
import org.jboss.aesh.terminal.TerminalColor;
|
||||
import org.jboss.aesh.terminal.TerminalString;
|
||||
import org.keycloak.client.registration.Auth;
|
||||
import org.keycloak.client.registration.ClientRegistration;
|
||||
import org.keycloak.client.registration.cli.commands.CreateCommand;
|
||||
import org.keycloak.client.registration.cli.commands.ExitCommand;
|
||||
import org.keycloak.client.registration.cli.commands.SetupCommand;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class ClientRegistrationCLI {
|
||||
|
||||
private static ClientRegistration reg;
|
||||
|
||||
public static void main(String[] args) throws CommandLineParserException, CommandNotFoundException {
|
||||
reg = ClientRegistration.create().url("http://localhost:8080/auth/realms/master").build();
|
||||
reg.auth(Auth.token("..."));
|
||||
|
||||
Context context = new Context();
|
||||
|
||||
List<Command> commands = new LinkedList<>();
|
||||
commands.add(new SetupCommand(context));
|
||||
commands.add(new CreateCommand(context));
|
||||
commands.add(new ExitCommand(context));
|
||||
|
||||
SettingsBuilder builder = new SettingsBuilder().logging(true);
|
||||
builder.enableMan(true).readInputrc(false);
|
||||
|
||||
Settings settings = builder.create();
|
||||
|
||||
AeshCommandRegistryBuilder commandRegistryBuilder = new AeshCommandRegistryBuilder();
|
||||
for (Command c : commands) {
|
||||
commandRegistryBuilder.command(c);
|
||||
}
|
||||
|
||||
AeshConsole aeshConsole = new AeshConsoleBuilder()
|
||||
.commandRegistry(commandRegistryBuilder.create())
|
||||
.settings(settings)
|
||||
.prompt(new Prompt(new TerminalString("[clientreg]$ ",
|
||||
new TerminalColor(Color.GREEN, Color.DEFAULT, Color.Intensity.BRIGHT))))
|
||||
.create();
|
||||
|
||||
aeshConsole.start();
|
||||
|
||||
|
||||
/*
|
||||
if (args.length > 0) {
|
||||
CommandContainer command = registry.getCommand(args[0], null);
|
||||
ParserGenerator.parseAndPopulate(command, args[0], Arrays.copyOfRange(args, 1, args.length));
|
||||
}*/
|
||||
|
||||
//commandInvocation.getCommandRegistry().getAllCommandNames()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package org.keycloak.client.registration.cli;
|
||||
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.codehaus.jackson.map.SerializationConfig;
|
||||
import org.codehaus.jackson.map.annotate.JsonSerialize;
|
||||
import org.keycloak.client.registration.ClientRegistration;
|
||||
import org.keycloak.util.SystemPropertiesJsonParserFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class Context {
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper(new SystemPropertiesJsonParserFactory());
|
||||
static {
|
||||
mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
|
||||
mapper.enable(SerializationConfig.Feature.INDENT_OUTPUT);
|
||||
}
|
||||
|
||||
private ClientRegistration reg;
|
||||
|
||||
public ClientRegistration getReg() {
|
||||
return reg;
|
||||
}
|
||||
|
||||
public void setReg(ClientRegistration reg) {
|
||||
this.reg = reg;
|
||||
}
|
||||
|
||||
public static <T> T readJson(InputStream bytes, Class<T> type) throws IOException {
|
||||
return mapper.readValue(bytes, type);
|
||||
}
|
||||
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package org.keycloak.client.registration.cli.commands;
|
||||
|
||||
import org.jboss.aesh.cl.Arguments;
|
||||
import org.jboss.aesh.cl.CommandDefinition;
|
||||
import org.jboss.aesh.cl.Option;
|
||||
import org.jboss.aesh.console.command.Command;
|
||||
import org.jboss.aesh.console.command.CommandResult;
|
||||
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||
import org.jboss.aesh.io.Resource;
|
||||
import org.keycloak.client.registration.ClientRegistrationException;
|
||||
import org.keycloak.client.registration.cli.Context;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
@CommandDefinition(name="create", description = "[OPTIONS] FILE")
|
||||
public class CreateCommand implements Command {
|
||||
|
||||
@Option(shortName = 'h', hasValue = false, description = "display this help and exit")
|
||||
private boolean help;
|
||||
|
||||
@Arguments(description = "files or directories thats listed")
|
||||
private List<Resource> arguments;
|
||||
|
||||
private Context context;
|
||||
|
||||
public CreateCommand(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
|
||||
System.out.println(help);
|
||||
|
||||
|
||||
if(help) {
|
||||
commandInvocation.getShell().out().println(commandInvocation.getHelpInfo("create"));
|
||||
}
|
||||
else {
|
||||
|
||||
if(arguments != null) {
|
||||
for(Resource f : arguments) {
|
||||
System.out.println(f.getAbsolutePath());
|
||||
ClientRepresentation rep = JsonSerialization.readValue(f.read(), ClientRepresentation.class);
|
||||
try {
|
||||
context.getReg().create(rep);
|
||||
} catch (ClientRegistrationException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// reg.create();
|
||||
|
||||
return CommandResult.SUCCESS;
|
||||
}
|
||||
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package org.keycloak.client.registration.cli.commands;
|
||||
|
||||
import org.jboss.aesh.cl.CommandDefinition;
|
||||
import org.jboss.aesh.console.command.Command;
|
||||
import org.jboss.aesh.console.command.CommandResult;
|
||||
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||
import org.keycloak.client.registration.cli.Context;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
@CommandDefinition(name="exit", description = "Exit the program")
|
||||
public class ExitCommand implements Command {
|
||||
|
||||
private Context context;
|
||||
|
||||
public ExitCommand(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
|
||||
commandInvocation.stop();
|
||||
return CommandResult.SUCCESS;
|
||||
}
|
||||
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package org.keycloak.client.registration.cli.commands;
|
||||
|
||||
import org.jboss.aesh.cl.CommandDefinition;
|
||||
import org.jboss.aesh.cl.Option;
|
||||
import org.jboss.aesh.console.command.Command;
|
||||
import org.jboss.aesh.console.command.CommandResult;
|
||||
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||
import org.jboss.aesh.io.Resource;
|
||||
import org.keycloak.client.registration.ClientRegistrationException;
|
||||
import org.keycloak.client.registration.cli.Context;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
@CommandDefinition(name="setup", description = "")
|
||||
public class SetupCommand implements Command {
|
||||
|
||||
@Option(shortName = 'h', hasValue = false, description = "display this help and exit")
|
||||
private boolean help;
|
||||
|
||||
private Context context;
|
||||
|
||||
public SetupCommand(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
|
||||
System.out.println(help);
|
||||
|
||||
if(help) {
|
||||
commandInvocation.getShell().out().println(commandInvocation.getHelpInfo("create"));
|
||||
}
|
||||
|
||||
return CommandResult.SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
private String promptForUsername(CommandInvocation invocation) throws InterruptedException {
|
||||
invocation.print("username: ");
|
||||
return invocation.getInputLine();
|
||||
}
|
||||
|
||||
}
|
||||
Regular → Executable
+7
-9
@@ -1,21 +1,19 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>distribution-pom</artifactId>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.3.0.Final-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
<version>1.8.0.CR3</version>
|
||||
</parent>
|
||||
<name>Feature Pack Builds</name>
|
||||
|
||||
<name>Keycloak Client Registration Parent</name>
|
||||
<description/>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>feature-pack-builds</artifactId>
|
||||
<artifactId>keycloak-client-registration-parent</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>adapter-only</module>
|
||||
<module>server-only</module>
|
||||
<module>server-and-adapter</module>
|
||||
<module>api</module>
|
||||
<!--<module>cli</module>-->
|
||||
</modules>
|
||||
</project>
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.8.0.CR3</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-common</artifactId>
|
||||
<name>Keycloak Common</name>
|
||||
<packaging>jar</packaging>
|
||||
<description>Common library and dependencies shared with server and all adapters</description>
|
||||
|
||||
<properties>
|
||||
<timestamp>${maven.build.timestamp}</timestamp>
|
||||
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
||||
<keycloak.osgi.export>
|
||||
org.keycloak.common.*
|
||||
</keycloak.osgi.export>
|
||||
<keycloak.osgi.import>
|
||||
*;resolution:=optional
|
||||
</keycloak.osgi.import>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk15on</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- Adding OSGI metadata to the JAR without changing the packaging type. -->
|
||||
<plugin>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.felix</groupId>
|
||||
<artifactId>maven-bundle-plugin</artifactId>
|
||||
<extensions>true</extensions>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>bundle-manifest</id>
|
||||
<phase>process-classes</phase>
|
||||
<goals>
|
||||
<goal>manifest</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<instructions>
|
||||
<Bundle-ClassPath>.</Bundle-ClassPath>
|
||||
<Bundle-Name>${project.name}</Bundle-Name>
|
||||
<Bundle-SymbolicName>${project.groupId}.${project.artifactId}</Bundle-SymbolicName>
|
||||
<Import-Package>${keycloak.osgi.import}</Import-Package>
|
||||
<Export-Package>${keycloak.osgi.export}</Export-Package>
|
||||
</instructions>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
+13
-13
@@ -1,13 +1,13 @@
|
||||
package org.keycloak;
|
||||
|
||||
/**
|
||||
* Information about the client connection
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface ClientConnection {
|
||||
String getRemoteAddr();
|
||||
String getRemoteHost();
|
||||
int getReportPort();
|
||||
}
|
||||
package org.keycloak.common;
|
||||
|
||||
/**
|
||||
* Information about the client connection
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface ClientConnection {
|
||||
String getRemoteAddr();
|
||||
String getRemoteHost();
|
||||
int getReportPort();
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak;
|
||||
package org.keycloak.common;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package org.keycloak.common;
|
||||
|
||||
import org.keycloak.common.util.Time;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class Version {
|
||||
public static final String UNKNOWN = "UNKNOWN";
|
||||
public static String NAME;
|
||||
public static String NAME_HTML;
|
||||
public static String VERSION;
|
||||
public static String RESOURCES_VERSION;
|
||||
public static String BUILD_TIME;
|
||||
|
||||
static {
|
||||
Properties props = new Properties();
|
||||
InputStream is = Version.class.getResourceAsStream("/keycloak-version.properties");
|
||||
try {
|
||||
props.load(is);
|
||||
Version.NAME = props.getProperty("name");
|
||||
Version.NAME_HTML = props.getProperty("name-html");
|
||||
Version.VERSION = props.getProperty("version");
|
||||
Version.BUILD_TIME = props.getProperty("build-time");
|
||||
Version.RESOURCES_VERSION = Version.VERSION.toLowerCase();
|
||||
if (Version.RESOURCES_VERSION.endsWith("-snapshot")) {
|
||||
Version.RESOURCES_VERSION = Version.RESOURCES_VERSION.replace("-snapshot", "-" + Time.currentTime());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Version.VERSION= Version.UNKNOWN;
|
||||
Version.BUILD_TIME= Version.UNKNOWN;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.constants;
|
||||
package org.keycloak.common.constants;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.constants;
|
||||
package org.keycloak.common.constants;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.keycloak.common.constants;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface ServiceAccountConstants {
|
||||
|
||||
String CLIENT_AUTH = "client_auth";
|
||||
|
||||
String SERVICE_ACCOUNT_USER_PREFIX = "service-account-";
|
||||
|
||||
String CLIENT_ID_PROTOCOL_MAPPER = "Client ID";
|
||||
String CLIENT_HOST_PROTOCOL_MAPPER = "Client Host";
|
||||
String CLIENT_ADDRESS_PROTOCOL_MAPPER = "Client IP Address";
|
||||
String CLIENT_ID = "clientId";
|
||||
String CLIENT_HOST = "clientHost";
|
||||
String CLIENT_ADDRESS = "clientAddress";
|
||||
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.enums;
|
||||
package org.keycloak.common.enums;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
+2
-4
@@ -1,6 +1,6 @@
|
||||
package org.keycloak.enums;
|
||||
package org.keycloak.common.enums;
|
||||
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
@@ -14,8 +14,6 @@ public enum SslRequired {
|
||||
EXTERNAL,
|
||||
NONE;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public boolean isRequired(ClientConnection connection) {
|
||||
return isRequired(connection.getRemoteAddr());
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+42
-44
@@ -1,44 +1,42 @@
|
||||
package org.keycloak.util;
|
||||
|
||||
|
||||
import net.iharder.Base64;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class Base64Url {
|
||||
public static String encode(byte[] bytes) {
|
||||
String s = Base64.encodeBytes(bytes);
|
||||
s = s.split("=")[0]; // Remove any trailing '='s
|
||||
s = s.replace('+', '-'); // 62nd char of encoding
|
||||
s = s.replace('/', '_'); // 63rd char of encoding
|
||||
return s;
|
||||
}
|
||||
|
||||
public static byte[] decode(String s) {
|
||||
s = s.replace('-', '+'); // 62nd char of encoding
|
||||
s = s.replace('_', '/'); // 63rd char of encoding
|
||||
switch (s.length() % 4) // Pad with trailing '='s
|
||||
{
|
||||
case 0:
|
||||
break; // No pad chars in this case
|
||||
case 2:
|
||||
s += "==";
|
||||
break; // Two pad chars
|
||||
case 3:
|
||||
s += "=";
|
||||
break; // One pad char
|
||||
default:
|
||||
throw new RuntimeException(
|
||||
"Illegal base64url string!");
|
||||
}
|
||||
try {
|
||||
return Base64.decode(s);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
package org.keycloak.common.util;
|
||||
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class Base64Url {
|
||||
public static String encode(byte[] bytes) {
|
||||
String s = Base64.encodeBytes(bytes);
|
||||
s = s.split("=")[0]; // Remove any trailing '='s
|
||||
s = s.replace('+', '-'); // 62nd char of encoding
|
||||
s = s.replace('/', '_'); // 63rd char of encoding
|
||||
return s;
|
||||
}
|
||||
|
||||
public static byte[] decode(String s) {
|
||||
s = s.replace('-', '+'); // 62nd char of encoding
|
||||
s = s.replace('_', '/'); // 63rd char of encoding
|
||||
switch (s.length() % 4) // Pad with trailing '='s
|
||||
{
|
||||
case 0:
|
||||
break; // No pad chars in this case
|
||||
case 2:
|
||||
s += "==";
|
||||
break; // Two pad chars
|
||||
case 3:
|
||||
s += "=";
|
||||
break; // One pad char
|
||||
default:
|
||||
throw new RuntimeException(
|
||||
"Illegal base64url string!");
|
||||
}
|
||||
try {
|
||||
return Base64.decode(s);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
+165
-166
@@ -1,166 +1,165 @@
|
||||
package org.keycloak.util;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
||||
import org.bouncycastle.asn1.x509.BasicConstraints;
|
||||
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.KeyPurposeId;
|
||||
import org.bouncycastle.asn1.x509.KeyUsage;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.cert.X509ExtensionUtils;
|
||||
import org.bouncycastle.cert.X509v1CertificateBuilder;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.crypto.util.PrivateKeyFactory;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
|
||||
import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
|
||||
import org.bouncycastle.operator.DigestCalculator;
|
||||
import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
|
||||
import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
|
||||
/**
|
||||
* The Class CertificateUtils provides utility functions for generation of V1 and V3 {@link java.security.cert.X509Certificate}
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @author <a href="mailto:giriraj.sharma27@gmail.com">Giriraj Sharma</a>
|
||||
* @version $Revision: 2 $
|
||||
*/
|
||||
public class CertificateUtils {
|
||||
static {
|
||||
BouncyIntegration.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates version 3 {@link java.security.cert.X509Certificate}.
|
||||
*
|
||||
* @param keyPair the key pair
|
||||
* @param caPrivateKey the CA private key
|
||||
* @param caCert the CA certificate
|
||||
* @param subject the subject name
|
||||
*
|
||||
* @return the x509 certificate
|
||||
*
|
||||
* @throws Exception the exception
|
||||
*/
|
||||
public static X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPrivateKey, X509Certificate caCert,
|
||||
String subject) throws Exception {
|
||||
|
||||
try {
|
||||
X500Name subjectDN = new X500Name("CN=" + subject);
|
||||
|
||||
// Serial Number
|
||||
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
||||
BigInteger serialNumber = BigInteger.valueOf(Math.abs(random.nextInt()));
|
||||
|
||||
// Validity
|
||||
Date notBefore = new Date(System.currentTimeMillis());
|
||||
Date notAfter = new Date(System.currentTimeMillis() + (((1000L * 60 * 60 * 24 * 30)) * 12) * 3);
|
||||
|
||||
// SubjectPublicKeyInfo
|
||||
SubjectPublicKeyInfo subjPubKeyInfo = new SubjectPublicKeyInfo(ASN1Sequence.getInstance(keyPair.getPublic()
|
||||
.getEncoded()));
|
||||
|
||||
X509v3CertificateBuilder certGen = new X509v3CertificateBuilder(new X500Name(caCert.getSubjectDN().getName()),
|
||||
serialNumber, notBefore, notAfter, subjectDN, subjPubKeyInfo);
|
||||
|
||||
DigestCalculator digCalc = new BcDigestCalculatorProvider()
|
||||
.get(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1));
|
||||
X509ExtensionUtils x509ExtensionUtils = new X509ExtensionUtils(digCalc);
|
||||
|
||||
// Subject Key Identifier
|
||||
certGen.addExtension(Extension.subjectKeyIdentifier, false,
|
||||
x509ExtensionUtils.createSubjectKeyIdentifier(subjPubKeyInfo));
|
||||
|
||||
// Authority Key Identifier
|
||||
certGen.addExtension(Extension.authorityKeyIdentifier, false,
|
||||
x509ExtensionUtils.createAuthorityKeyIdentifier(subjPubKeyInfo));
|
||||
|
||||
// Key Usage
|
||||
certGen.addExtension(Extension.keyUsage, false, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign
|
||||
| KeyUsage.cRLSign));
|
||||
|
||||
// Extended Key Usage
|
||||
KeyPurposeId[] EKU = new KeyPurposeId[2];
|
||||
EKU[0] = KeyPurposeId.id_kp_emailProtection;
|
||||
EKU[1] = KeyPurposeId.id_kp_serverAuth;
|
||||
|
||||
certGen.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(EKU));
|
||||
|
||||
// Basic Constraints
|
||||
certGen.addExtension(Extension.basicConstraints, true, new BasicConstraints(0));
|
||||
|
||||
// Content Signer
|
||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1WithRSAEncryption").setProvider("BC").build(caPrivateKey);
|
||||
|
||||
// Certificate
|
||||
return new JcaX509CertificateConverter().setProvider("BC").getCertificate(certGen.build(sigGen));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error creating X509v3Certificate.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate version 1 self signed {@link java.security.cert.X509Certificate}..
|
||||
*
|
||||
* @param caKeyPair the CA key pair
|
||||
* @param subject the subject name
|
||||
*
|
||||
* @return the x509 certificate
|
||||
*
|
||||
* @throws Exception the exception
|
||||
*/
|
||||
public static X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject) throws Exception {
|
||||
|
||||
try {
|
||||
X500Name subjectDN = new X500Name("CN=" + subject);
|
||||
BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis());
|
||||
Date validityStartDate = new Date(System.currentTimeMillis() - 100000);
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(Calendar.YEAR, 10);
|
||||
Date validityEndDate = new Date(calendar.getTime().getTime());
|
||||
SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(caKeyPair.getPublic().getEncoded());
|
||||
|
||||
X509v1CertificateBuilder builder = new X509v1CertificateBuilder(subjectDN, serialNumber, validityStartDate,
|
||||
validityEndDate, subjectDN, subPubKeyInfo);
|
||||
X509CertificateHolder holder = builder.build(createSigner(caKeyPair.getPrivate()));
|
||||
|
||||
return new JcaX509CertificateConverter().getCertificate(holder);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error creating X509v1Certificate.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the content signer for generation of Version 1 {@link java.security.cert.X509Certificate}.
|
||||
*
|
||||
* @param privateKey the private key
|
||||
*
|
||||
* @return the content signer
|
||||
*/
|
||||
public static ContentSigner createSigner(PrivateKey privateKey) {
|
||||
try {
|
||||
AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256WithRSAEncryption");
|
||||
AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);
|
||||
|
||||
return new BcRSAContentSignerBuilder(sigAlgId, digAlgId)
|
||||
.build(PrivateKeyFactory.createKey(privateKey.getEncoded()));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Could not create content signer.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
||||
import org.bouncycastle.asn1.x509.BasicConstraints;
|
||||
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.KeyPurposeId;
|
||||
import org.bouncycastle.asn1.x509.KeyUsage;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.cert.X509ExtensionUtils;
|
||||
import org.bouncycastle.cert.X509v1CertificateBuilder;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.crypto.util.PrivateKeyFactory;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
|
||||
import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
|
||||
import org.bouncycastle.operator.DigestCalculator;
|
||||
import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
|
||||
import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
|
||||
/**
|
||||
* The Class CertificateUtils provides utility functions for generation of V1 and V3 {@link java.security.cert.X509Certificate}
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @author <a href="mailto:giriraj.sharma27@gmail.com">Giriraj Sharma</a>
|
||||
* @version $Revision: 2 $
|
||||
*/
|
||||
public class CertificateUtils {
|
||||
static {
|
||||
BouncyIntegration.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates version 3 {@link java.security.cert.X509Certificate}.
|
||||
*
|
||||
* @param keyPair the key pair
|
||||
* @param caPrivateKey the CA private key
|
||||
* @param caCert the CA certificate
|
||||
* @param subject the subject name
|
||||
*
|
||||
* @return the x509 certificate
|
||||
*
|
||||
* @throws Exception the exception
|
||||
*/
|
||||
public static X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPrivateKey, X509Certificate caCert,
|
||||
String subject) throws Exception {
|
||||
|
||||
try {
|
||||
X500Name subjectDN = new X500Name("CN=" + subject);
|
||||
|
||||
// Serial Number
|
||||
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
||||
BigInteger serialNumber = BigInteger.valueOf(Math.abs(random.nextInt()));
|
||||
|
||||
// Validity
|
||||
Date notBefore = new Date(System.currentTimeMillis());
|
||||
Date notAfter = new Date(System.currentTimeMillis() + (((1000L * 60 * 60 * 24 * 30)) * 12) * 3);
|
||||
|
||||
// SubjectPublicKeyInfo
|
||||
SubjectPublicKeyInfo subjPubKeyInfo = new SubjectPublicKeyInfo(ASN1Sequence.getInstance(keyPair.getPublic()
|
||||
.getEncoded()));
|
||||
|
||||
X509v3CertificateBuilder certGen = new X509v3CertificateBuilder(new X500Name(caCert.getSubjectDN().getName()),
|
||||
serialNumber, notBefore, notAfter, subjectDN, subjPubKeyInfo);
|
||||
|
||||
DigestCalculator digCalc = new BcDigestCalculatorProvider()
|
||||
.get(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1));
|
||||
X509ExtensionUtils x509ExtensionUtils = new X509ExtensionUtils(digCalc);
|
||||
|
||||
// Subject Key Identifier
|
||||
certGen.addExtension(Extension.subjectKeyIdentifier, false,
|
||||
x509ExtensionUtils.createSubjectKeyIdentifier(subjPubKeyInfo));
|
||||
|
||||
// Authority Key Identifier
|
||||
certGen.addExtension(Extension.authorityKeyIdentifier, false,
|
||||
x509ExtensionUtils.createAuthorityKeyIdentifier(subjPubKeyInfo));
|
||||
|
||||
// Key Usage
|
||||
certGen.addExtension(Extension.keyUsage, false, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign
|
||||
| KeyUsage.cRLSign));
|
||||
|
||||
// Extended Key Usage
|
||||
KeyPurposeId[] EKU = new KeyPurposeId[2];
|
||||
EKU[0] = KeyPurposeId.id_kp_emailProtection;
|
||||
EKU[1] = KeyPurposeId.id_kp_serverAuth;
|
||||
|
||||
certGen.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(EKU));
|
||||
|
||||
// Basic Constraints
|
||||
certGen.addExtension(Extension.basicConstraints, true, new BasicConstraints(0));
|
||||
|
||||
// Content Signer
|
||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1WithRSAEncryption").setProvider("BC").build(caPrivateKey);
|
||||
|
||||
// Certificate
|
||||
return new JcaX509CertificateConverter().setProvider("BC").getCertificate(certGen.build(sigGen));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error creating X509v3Certificate.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate version 1 self signed {@link java.security.cert.X509Certificate}..
|
||||
*
|
||||
* @param caKeyPair the CA key pair
|
||||
* @param subject the subject name
|
||||
*
|
||||
* @return the x509 certificate
|
||||
*
|
||||
* @throws Exception the exception
|
||||
*/
|
||||
public static X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject) throws Exception {
|
||||
|
||||
try {
|
||||
X500Name subjectDN = new X500Name("CN=" + subject);
|
||||
BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis());
|
||||
Date validityStartDate = new Date(System.currentTimeMillis() - 100000);
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(Calendar.YEAR, 10);
|
||||
Date validityEndDate = new Date(calendar.getTime().getTime());
|
||||
SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(caKeyPair.getPublic().getEncoded());
|
||||
|
||||
X509v1CertificateBuilder builder = new X509v1CertificateBuilder(subjectDN, serialNumber, validityStartDate,
|
||||
validityEndDate, subjectDN, subPubKeyInfo);
|
||||
X509CertificateHolder holder = builder.build(createSigner(caKeyPair.getPrivate()));
|
||||
|
||||
return new JcaX509CertificateConverter().getCertificate(holder);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error creating X509v1Certificate.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the content signer for generation of Version 1 {@link java.security.cert.X509Certificate}.
|
||||
*
|
||||
* @param privateKey the private key
|
||||
*
|
||||
* @return the content signer
|
||||
*/
|
||||
public static ContentSigner createSigner(PrivateKey privateKey) {
|
||||
try {
|
||||
AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256WithRSAEncryption");
|
||||
AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);
|
||||
|
||||
return new BcRSAContentSignerBuilder(sigAlgId, digAlgId)
|
||||
.build(PrivateKeyFactory.createKey(privateKey.getEncoded()));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Could not create content signer.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
@@ -23,4 +23,19 @@ public class CollectionUtil {
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// Return true if all items from col1 are in col2 and viceversa. Order is not taken into account
|
||||
public static <T> boolean collectionEquals(Collection<T> col1, Collection<T> col2) {
|
||||
if (col1.size() != col2.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (T item : col1) {
|
||||
if (!col2.contains(item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.InputStream;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
+37
-37
@@ -1,37 +1,37 @@
|
||||
package org.keycloak.util;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public final class EnvUtil {
|
||||
private static final Pattern p = Pattern.compile("[$][{]([^}]+)[}]");
|
||||
|
||||
private EnvUtil() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces any ${} strings with their corresponding environent variable.
|
||||
*
|
||||
* @param val
|
||||
* @return
|
||||
*/
|
||||
public static String replace(String val) {
|
||||
Matcher matcher = p.matcher(val);
|
||||
StringBuffer buf = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
String envVar = matcher.group(1);
|
||||
String envVal = System.getProperty(envVar);
|
||||
if (envVal == null) envVal = "NOT-SPECIFIED";
|
||||
matcher.appendReplacement(buf, envVal.replace("\\", "\\\\"));
|
||||
}
|
||||
matcher.appendTail(buf);
|
||||
return buf.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public final class EnvUtil {
|
||||
private static final Pattern p = Pattern.compile("[$][{]([^}]+)[}]");
|
||||
|
||||
private EnvUtil() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces any ${} strings with their corresponding environent variable.
|
||||
*
|
||||
* @param val
|
||||
* @return
|
||||
*/
|
||||
public static String replace(String val) {
|
||||
Matcher matcher = p.matcher(val);
|
||||
StringBuffer buf = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
String envVar = matcher.group(1);
|
||||
String envVal = System.getProperty(envVar);
|
||||
if (envVal == null) envVal = "NOT-SPECIFIED";
|
||||
matcher.appendReplacement(buf, envVal.replace("\\", "\\\\"));
|
||||
}
|
||||
matcher.appendTail(buf);
|
||||
return buf.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+37
-37
@@ -1,37 +1,37 @@
|
||||
package org.keycloak.util;
|
||||
|
||||
import org.keycloak.constants.GenericConstants;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class FindFile {
|
||||
public static InputStream findFile(String keycloakConfigFile) {
|
||||
if (keycloakConfigFile.startsWith(GenericConstants.PROTOCOL_CLASSPATH)) {
|
||||
String classPathLocation = keycloakConfigFile.replace(GenericConstants.PROTOCOL_CLASSPATH, "");
|
||||
// Try current class classloader first
|
||||
InputStream is = FindFile.class.getClassLoader().getResourceAsStream(classPathLocation);
|
||||
if (is == null) {
|
||||
is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classPathLocation);
|
||||
}
|
||||
|
||||
if (is != null) {
|
||||
return is;
|
||||
} else {
|
||||
throw new RuntimeException("Unable to find config from classpath: " + keycloakConfigFile);
|
||||
}
|
||||
} else {
|
||||
// Fallback to file
|
||||
try {
|
||||
return new FileInputStream(keycloakConfigFile);
|
||||
} catch (FileNotFoundException fnfe) {
|
||||
throw new RuntimeException(fnfe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import org.keycloak.common.constants.GenericConstants;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class FindFile {
|
||||
public static InputStream findFile(String keycloakConfigFile) {
|
||||
if (keycloakConfigFile.startsWith(GenericConstants.PROTOCOL_CLASSPATH)) {
|
||||
String classPathLocation = keycloakConfigFile.replace(GenericConstants.PROTOCOL_CLASSPATH, "");
|
||||
// Try current class classloader first
|
||||
InputStream is = FindFile.class.getClassLoader().getResourceAsStream(classPathLocation);
|
||||
if (is == null) {
|
||||
is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classPathLocation);
|
||||
}
|
||||
|
||||
if (is != null) {
|
||||
return is;
|
||||
} else {
|
||||
throw new RuntimeException("Unable to find config from classpath: " + keycloakConfigFile);
|
||||
}
|
||||
} else {
|
||||
// Fallback to file
|
||||
try {
|
||||
return new FileInputStream(keycloakConfigFile);
|
||||
} catch (FileNotFoundException fnfe) {
|
||||
throw new RuntimeException(fnfe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
/**
|
||||
* @author pedroigor
|
||||
+3
-4
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -12,13 +12,12 @@ import java.lang.reflect.Method;
|
||||
|
||||
import javax.security.auth.kerberos.KerberosTicket;
|
||||
|
||||
import net.iharder.Base64;
|
||||
import org.ietf.jgss.GSSCredential;
|
||||
import org.ietf.jgss.GSSException;
|
||||
import org.ietf.jgss.GSSManager;
|
||||
import org.ietf.jgss.Oid;
|
||||
import org.keycloak.constants.KerberosConstants;
|
||||
import org.keycloak.util.reflections.Reflections;
|
||||
import org.keycloak.common.constants.KerberosConstants;
|
||||
import org.keycloak.common.util.reflections.Reflections;
|
||||
import sun.security.jgss.GSSCredentialImpl;
|
||||
import sun.security.jgss.GSSManagerImpl;
|
||||
import sun.security.jgss.krb5.Krb5InitCredential;
|
||||
+724
-713
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,56 @@
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
|
||||
import org.keycloak.common.constants.GenericConstants;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class KeystoreUtil {
|
||||
static {
|
||||
BouncyIntegration.init();
|
||||
}
|
||||
|
||||
public enum KeystoreFormat {
|
||||
JKS,
|
||||
PKCS12
|
||||
}
|
||||
|
||||
public static KeyStore loadKeyStore(String filename, String password) throws Exception {
|
||||
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
InputStream trustStream = (filename.startsWith(GenericConstants.PROTOCOL_CLASSPATH))
|
||||
?KeystoreUtil.class.getResourceAsStream(filename.replace(GenericConstants.PROTOCOL_CLASSPATH, ""))
|
||||
:new FileInputStream(new File(filename));
|
||||
trustStore.load(trustStream, password.toCharArray());
|
||||
trustStream.close();
|
||||
return trustStore;
|
||||
}
|
||||
|
||||
public static PrivateKey loadPrivateKeyFromKeystore(String keystoreFile, String storePassword, String keyPassword, String keyAlias, KeystoreFormat format) {
|
||||
InputStream stream = FindFile.findFile(keystoreFile);
|
||||
|
||||
try {
|
||||
KeyStore keyStore = null;
|
||||
if (format == KeystoreFormat.JKS) {
|
||||
keyStore = KeyStore.getInstance(format.toString());
|
||||
} else {
|
||||
keyStore = KeyStore.getInstance(format.toString(), "BC");
|
||||
}
|
||||
|
||||
keyStore.load(stream, storePassword.toCharArray());
|
||||
PrivateKey key = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray());
|
||||
if (key == null) {
|
||||
throw new RuntimeException("Couldn't load key with alias '" + keyAlias + "' from keystore");
|
||||
}
|
||||
return key;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load private key: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import javax.activation.MimetypesFileTypeMap;
|
||||
import java.io.File;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
/*
|
||||
* JBoss, Home of Professional Open Source.
|
||||
+6
-2
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
@@ -13,7 +13,7 @@ public class ObjectUtil {
|
||||
* @param str2
|
||||
* @return true if both strings are null or equal
|
||||
*/
|
||||
public static boolean isEqualOrNull(Object str1, Object str2) {
|
||||
public static boolean isEqualOrBothNull(Object str1, Object str2) {
|
||||
if (str1 == null && str2 == null) {
|
||||
return true;
|
||||
}
|
||||
@@ -24,4 +24,8 @@ public class ObjectUtil {
|
||||
|
||||
return str1.equals(str2);
|
||||
}
|
||||
|
||||
public static String capitalize(String str) {
|
||||
return str.substring(0, 1).toUpperCase() + str.substring(1);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
*
|
||||
*/
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
+108
-110
@@ -1,110 +1,108 @@
|
||||
package org.keycloak.util;
|
||||
|
||||
|
||||
import net.iharder.Base64;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
/**
|
||||
* Utility classes to extract PublicKey, PrivateKey, and X509Certificate from openssl generated PEM files
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public final class PemUtils {
|
||||
static {
|
||||
BouncyIntegration.init();
|
||||
}
|
||||
|
||||
private PemUtils() {
|
||||
}
|
||||
|
||||
public static X509Certificate decodeCertificate(InputStream is) throws Exception {
|
||||
byte[] der = pemToDer(is);
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(der);
|
||||
return DerUtils.decodeCertificate(bis);
|
||||
}
|
||||
|
||||
public static X509Certificate decodeCertificate(String cert) throws Exception {
|
||||
byte[] der = pemToDer(cert);
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(der);
|
||||
return DerUtils.decodeCertificate(bis);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extract a public key from a PEM string
|
||||
*
|
||||
* @param pem
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static PublicKey decodePublicKey(String pem) throws Exception {
|
||||
byte[] der = pemToDer(pem);
|
||||
return DerUtils.decodePublicKey(der);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a private key that is a PKCS8 pem string (base64 encoded PKCS8)
|
||||
*
|
||||
* @param pem
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static PrivateKey decodePrivateKey(String pem) throws Exception {
|
||||
byte[] der = pemToDer(pem);
|
||||
return DerUtils.decodePrivateKey(der);
|
||||
}
|
||||
|
||||
public static PrivateKey decodePrivateKey(InputStream is) throws Exception {
|
||||
String pem = pemFromStream(is);
|
||||
return decodePrivateKey(pem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a PEM file to DER format
|
||||
*
|
||||
* @param is
|
||||
* @return
|
||||
* @throws java.io.IOException
|
||||
*/
|
||||
public static byte[] pemToDer(InputStream is) throws IOException {
|
||||
String pem = pemFromStream(is);
|
||||
return pemToDer(pem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a PEM string to DER format
|
||||
*
|
||||
* @param pem
|
||||
* @return
|
||||
* @throws java.io.IOException
|
||||
*/
|
||||
public static byte[] pemToDer(String pem) throws IOException {
|
||||
pem = removeBeginEnd(pem);
|
||||
return Base64.decode(pem);
|
||||
}
|
||||
|
||||
public static String removeBeginEnd(String pem) {
|
||||
pem = pem.replaceAll("-----BEGIN (.*)-----", "");
|
||||
pem = pem.replaceAll("-----END (.*)----", "");
|
||||
pem = pem.replaceAll("\r\n", "");
|
||||
pem = pem.replaceAll("\n", "");
|
||||
return pem.trim();
|
||||
}
|
||||
|
||||
|
||||
public static String pemFromStream(InputStream is) throws IOException {
|
||||
DataInputStream dis = new DataInputStream(is);
|
||||
byte[] keyBytes = new byte[dis.available()];
|
||||
dis.readFully(keyBytes);
|
||||
dis.close();
|
||||
return new String(keyBytes, "utf-8");
|
||||
}
|
||||
}
|
||||
package org.keycloak.common.util;
|
||||
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
/**
|
||||
* Utility classes to extract PublicKey, PrivateKey, and X509Certificate from openssl generated PEM files
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public final class PemUtils {
|
||||
static {
|
||||
BouncyIntegration.init();
|
||||
}
|
||||
|
||||
private PemUtils() {
|
||||
}
|
||||
|
||||
public static X509Certificate decodeCertificate(InputStream is) throws Exception {
|
||||
byte[] der = pemToDer(is);
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(der);
|
||||
return DerUtils.decodeCertificate(bis);
|
||||
}
|
||||
|
||||
public static X509Certificate decodeCertificate(String cert) throws Exception {
|
||||
byte[] der = pemToDer(cert);
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(der);
|
||||
return DerUtils.decodeCertificate(bis);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extract a public key from a PEM string
|
||||
*
|
||||
* @param pem
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static PublicKey decodePublicKey(String pem) throws Exception {
|
||||
byte[] der = pemToDer(pem);
|
||||
return DerUtils.decodePublicKey(der);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a private key that is a PKCS8 pem string (base64 encoded PKCS8)
|
||||
*
|
||||
* @param pem
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static PrivateKey decodePrivateKey(String pem) throws Exception {
|
||||
byte[] der = pemToDer(pem);
|
||||
return DerUtils.decodePrivateKey(der);
|
||||
}
|
||||
|
||||
public static PrivateKey decodePrivateKey(InputStream is) throws Exception {
|
||||
String pem = pemFromStream(is);
|
||||
return decodePrivateKey(pem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a PEM file to DER format
|
||||
*
|
||||
* @param is
|
||||
* @return
|
||||
* @throws java.io.IOException
|
||||
*/
|
||||
public static byte[] pemToDer(InputStream is) throws IOException {
|
||||
String pem = pemFromStream(is);
|
||||
return pemToDer(pem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a PEM string to DER format
|
||||
*
|
||||
* @param pem
|
||||
* @return
|
||||
* @throws java.io.IOException
|
||||
*/
|
||||
public static byte[] pemToDer(String pem) throws IOException {
|
||||
pem = removeBeginEnd(pem);
|
||||
return Base64.decode(pem);
|
||||
}
|
||||
|
||||
public static String removeBeginEnd(String pem) {
|
||||
pem = pem.replaceAll("-----BEGIN (.*)-----", "");
|
||||
pem = pem.replaceAll("-----END (.*)----", "");
|
||||
pem = pem.replaceAll("\r\n", "");
|
||||
pem = pem.replaceAll("\n", "");
|
||||
return pem.trim();
|
||||
}
|
||||
|
||||
|
||||
public static String pemFromStream(InputStream is) throws IOException {
|
||||
DataInputStream dis = new DataInputStream(is);
|
||||
byte[] keyBytes = new byte[dis.available()];
|
||||
dis.readFully(keyBytes);
|
||||
dis.close();
|
||||
return new String(keyBytes, "utf-8");
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.text.DateFormat;
|
||||
+35
-35
@@ -1,35 +1,35 @@
|
||||
package org.keycloak.util;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public final class StreamUtil {
|
||||
|
||||
private StreamUtil() {
|
||||
}
|
||||
|
||||
public static String readString(InputStream in) throws IOException
|
||||
{
|
||||
char[] buffer = new char[1024];
|
||||
StringBuilder builder = new StringBuilder();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
|
||||
int wasRead;
|
||||
do
|
||||
{
|
||||
wasRead = reader.read(buffer, 0, 1024);
|
||||
if (wasRead > 0)
|
||||
{
|
||||
builder.append(buffer, 0, wasRead);
|
||||
}
|
||||
}
|
||||
while (wasRead > -1);
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public final class StreamUtil {
|
||||
|
||||
private StreamUtil() {
|
||||
}
|
||||
|
||||
public static String readString(InputStream in) throws IOException
|
||||
{
|
||||
char[] buffer = new char[1024];
|
||||
StringBuilder builder = new StringBuilder();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
|
||||
int wasRead;
|
||||
do
|
||||
{
|
||||
wasRead = reader.read(buffer, 0, 1024);
|
||||
if (wasRead > 0)
|
||||
{
|
||||
builder.append(buffer, 0, wasRead);
|
||||
}
|
||||
}
|
||||
while (wasRead > -1);
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
||||
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
|
||||
*/
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Properties;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util;
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util.reflections;
|
||||
package org.keycloak.common.util.reflections;
|
||||
|
||||
import java.beans.Introspector;
|
||||
import java.io.Serializable;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util.reflections;
|
||||
package org.keycloak.common.util.reflections;
|
||||
|
||||
import java.lang.reflect.AccessibleObject;
|
||||
import java.security.PrivilegedAction;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.util.reflections;
|
||||
package org.keycloak.common.util.reflections;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
name=${product.name}
|
||||
name-html=${product.name-html}
|
||||
version=${product.version}
|
||||
build-time=${product.build-time}
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
|
||||
* as indicated by the @author tags. All rights reserved.
|
||||
*
|
||||
* 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.connections.file;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* Provides the InMemoryModel and notifies the factory to save it when
|
||||
* the session is done.
|
||||
*
|
||||
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
|
||||
*/
|
||||
public class DefaultFileConnectionProvider implements FileConnectionProvider {
|
||||
|
||||
private final DefaultFileConnectionProviderFactory factory;
|
||||
private final KeycloakSession session;
|
||||
private final InMemoryModel inMemoryModel;
|
||||
|
||||
private boolean isRollbackOnly = false;
|
||||
|
||||
public DefaultFileConnectionProvider(DefaultFileConnectionProviderFactory factory,
|
||||
KeycloakSession session,
|
||||
InMemoryModel inMemoryModel) {
|
||||
this.factory = factory;
|
||||
this.session = session;
|
||||
this.inMemoryModel = inMemoryModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InMemoryModel getModel() {
|
||||
return inMemoryModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionClosed(KeycloakSession session) {
|
||||
factory.sessionClosed(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void begin() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit() {
|
||||
factory.commit(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback() {
|
||||
factory.rollback(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRollbackOnly() {
|
||||
isRollbackOnly = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getRollbackOnly() {
|
||||
return isRollbackOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return factory.isActive(session);
|
||||
}
|
||||
|
||||
}
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
|
||||
* as indicated by the @author tags. All rights reserved.
|
||||
*
|
||||
* 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.connections.file;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.codehaus.jackson.JsonToken;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.exportimport.Strategy;
|
||||
import org.keycloak.exportimport.util.ExportUtils;
|
||||
import org.keycloak.exportimport.util.ImportUtils;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* This class dispenses a FileConnectionProvider to Keycloak sessions. It
|
||||
* makes sure that only one InMemoryModel is provided for each session and it
|
||||
* handles thread contention for the file where the model is read or saved.
|
||||
*
|
||||
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
|
||||
*/
|
||||
public class DefaultFileConnectionProviderFactory implements FileConnectionProviderFactory {
|
||||
|
||||
protected static final Logger logger = Logger.getLogger(DefaultFileConnectionProviderFactory.class);
|
||||
|
||||
private File kcdata;
|
||||
private final Map<KeycloakSession, FileConnectionProvider> allProviders = new HashMap<KeycloakSession, FileConnectionProvider>();
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
String fileName = config.get("fileName");
|
||||
if (fileName == null) {
|
||||
fileName = "keycloak-model.json";
|
||||
}
|
||||
|
||||
String directory = config.get("directory");
|
||||
if (directory == null) {
|
||||
directory = System.getProperty("jboss.server.data.dir");
|
||||
}
|
||||
if (directory == null) {
|
||||
directory = ".";
|
||||
}
|
||||
|
||||
kcdata = new File(directory, fileName);
|
||||
}
|
||||
|
||||
public void sessionClosed(KeycloakSession session) {
|
||||
synchronized(allProviders) {
|
||||
allProviders.remove(session);
|
||||
//logger.info("Removed session " + session.hashCode());
|
||||
//logger.info("sessionClosed: Session count=" + allModels.size());
|
||||
}
|
||||
}
|
||||
|
||||
void readModelFile(KeycloakSession session) {
|
||||
synchronized(allProviders) {
|
||||
if (!kcdata.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
fis = new FileInputStream(kcdata);
|
||||
Model model = JsonSerialization.readValue(fis, Model.class);
|
||||
ImportUtils.importFromStream(session, JsonSerialization.mapper, fis, Strategy.IGNORE_EXISTING);
|
||||
session.realms().getMigrationModel().setStoredVersion(model.getModelVersion());
|
||||
|
||||
List<RealmRepresentation> realmReps = new ArrayList<RealmRepresentation>();
|
||||
for (RealmRepresentation realmRep : model.getRealms()) {
|
||||
if (Config.getAdminRealm().equals(realmRep.getRealm())) {
|
||||
realmReps.add(0, realmRep);
|
||||
} else {
|
||||
realmReps.add(realmRep);
|
||||
}
|
||||
}
|
||||
for (RealmRepresentation realmRep : realmReps) {
|
||||
ImportUtils.importRealm(session, realmRep, Strategy.IGNORE_EXISTING);
|
||||
}
|
||||
|
||||
} catch (IOException ioe) {
|
||||
logger.error("Unable to read model file " + kcdata.getAbsolutePath(), ioe);
|
||||
} finally {
|
||||
//logger.info("Read model file for session=" + session.hashCode());
|
||||
try {
|
||||
if (fis != null) {
|
||||
fis.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to close output stream.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void writeModelFile(KeycloakSession session) {
|
||||
synchronized(allProviders) {
|
||||
FileOutputStream outStream = null;
|
||||
|
||||
try {
|
||||
outStream = new FileOutputStream(kcdata);
|
||||
exportModel(session, outStream);
|
||||
} catch (IOException e) {
|
||||
logger.error("Unable to write model file " + kcdata.getAbsolutePath(), e);
|
||||
} finally {
|
||||
//logger.info("Wrote model file for session=" + session.hashCode());
|
||||
try {
|
||||
if (outStream != null) {
|
||||
outStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to close output stream.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void exportModel(KeycloakSession session, FileOutputStream outStream) throws IOException {
|
||||
List<RealmModel> realms = session.realms().getRealms();
|
||||
List<RealmRepresentation> reps = new ArrayList<RealmRepresentation>();
|
||||
for (RealmModel realm : realms) {
|
||||
reps.add(ExportUtils.exportRealm(session, realm, true));
|
||||
}
|
||||
Model model = new Model();
|
||||
model.setRealms(reps);
|
||||
model.setModelVersion(session.realms().getMigrationModel().getStoredVersion());
|
||||
JsonSerialization.prettyMapper.writeValue(outStream, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileConnectionProvider create(KeycloakSession session) {
|
||||
synchronized (allProviders) {
|
||||
FileConnectionProvider fcProvider = allProviders.get(session);
|
||||
if (fcProvider == null) {
|
||||
InMemoryModel model = new InMemoryModel();
|
||||
fcProvider = new DefaultFileConnectionProvider(this, session, model);
|
||||
allProviders.put(session, fcProvider);
|
||||
session.getTransaction().enlist(fcProvider);
|
||||
readModelFile(session);
|
||||
//logger.info("Added session " + session.hashCode() + " total sessions=" + allModels.size());
|
||||
}
|
||||
|
||||
return fcProvider;
|
||||
}
|
||||
}
|
||||
|
||||
// commitCount is used for debugging. This allows you to easily run a test
|
||||
// to a particular point and then examine the JSON file.
|
||||
//private static int commitCount = 0;
|
||||
void commit(KeycloakSession session) {
|
||||
//commitCount++;
|
||||
synchronized (allProviders) {
|
||||
// in case commit was somehow called twice on the same session
|
||||
if (!allProviders.containsKey(session)) return;
|
||||
|
||||
try {
|
||||
writeModelFile(session);
|
||||
} finally {
|
||||
allProviders.remove(session);
|
||||
//logger.info("Removed session " + session.hashCode());
|
||||
//logger.info("*** commitCount=" + commitCount);
|
||||
//logger.info("commit(): Session count=" + allModels.size());
|
||||
}
|
||||
|
||||
// if (commitCount == 16) {Thread.dumpStack();System.exit(0);}
|
||||
}
|
||||
}
|
||||
|
||||
void rollback(KeycloakSession session) {
|
||||
synchronized (allProviders) {
|
||||
allProviders.remove(session);
|
||||
//logger.info("rollback(): Session count=" + allModels.size());
|
||||
}
|
||||
}
|
||||
|
||||
boolean isActive(KeycloakSession session) {
|
||||
synchronized (allProviders) {
|
||||
return allProviders.containsKey(session);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "default";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
|
||||
* as indicated by the @author tags. All rights reserved.
|
||||
*
|
||||
* 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.connections.file;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
/**
|
||||
* This class provides an in-memory copy of the entire model for each
|
||||
* Keycloak session. At the start of the session, the model is read
|
||||
* from JSON. When the session's transaction ends, the model is written back
|
||||
* out.
|
||||
*
|
||||
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
|
||||
*/
|
||||
public class InMemoryModel {
|
||||
private final Map<String, RealmModel> allRealms = new HashMap<String, RealmModel>();
|
||||
|
||||
// realmId, userId, userModel
|
||||
private final Map<String, Map<String,UserModel>> allUsers = new HashMap<String, Map<String,UserModel>>();
|
||||
|
||||
private String modelVersion;
|
||||
|
||||
public InMemoryModel() {
|
||||
}
|
||||
|
||||
public void putRealm(String id, RealmModel realm) {
|
||||
allRealms.put(id, realm);
|
||||
allUsers.put(id, new HashMap<String, UserModel>());
|
||||
}
|
||||
|
||||
public String getModelVersion() {
|
||||
return modelVersion;
|
||||
}
|
||||
|
||||
public void setModelVersion(String modelVersion) {
|
||||
this.modelVersion = modelVersion;
|
||||
}
|
||||
|
||||
public RealmModel getRealm(String id) {
|
||||
return allRealms.get(id);
|
||||
}
|
||||
|
||||
public Collection<RealmModel> getRealms() {
|
||||
return allRealms.values();
|
||||
}
|
||||
|
||||
public RealmModel getRealmByName(String name) {
|
||||
for (RealmModel realm : getRealms()) {
|
||||
if (realm.getName().equals(name)) return realm;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean removeRealm(String id) {
|
||||
allUsers.remove(id);
|
||||
return (allRealms.remove(id) != null);
|
||||
}
|
||||
|
||||
protected Map<String, UserModel> realmUsers(String realmId) {
|
||||
Map<String, UserModel> realmUsers = allUsers.get(realmId);
|
||||
if (realmUsers == null) throw new NullPointerException("Realm users not found for id=" + realmId);
|
||||
return realmUsers;
|
||||
}
|
||||
|
||||
public void putUser(String realmId, String userId, UserModel user) {
|
||||
realmUsers(realmId).put(userId, user);
|
||||
}
|
||||
|
||||
public UserModel getUser(String realmId, String userId) {
|
||||
return realmUsers(realmId).get(userId);
|
||||
}
|
||||
|
||||
public boolean hasUserWithUsername(String realmId, String username) {
|
||||
for (UserModel user : getUsers(realmId)) {
|
||||
if (user.getUsername().equals(username)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Collection<UserModel> getUsers(String realmId) {
|
||||
return realmUsers(realmId).values();
|
||||
}
|
||||
|
||||
public boolean removeUser(String realmId, String userId) {
|
||||
return (realmUsers(realmId).remove(userId) != null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package org.keycloak.connections.file;
|
||||
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class Model {
|
||||
private String modelVersion;
|
||||
private List<RealmRepresentation> realms;
|
||||
|
||||
public String getModelVersion() {
|
||||
return modelVersion;
|
||||
}
|
||||
|
||||
public void setModelVersion(String modelVersion) {
|
||||
this.modelVersion = modelVersion;
|
||||
}
|
||||
|
||||
public List<RealmRepresentation> getRealms() {
|
||||
return realms;
|
||||
}
|
||||
|
||||
public void setRealms(List<RealmRepresentation> realms) {
|
||||
this.realms = realms;
|
||||
}
|
||||
}
|
||||
Loaded 100 of 3734 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user