Compare commits
448
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f0ff23332 | ||
|
|
9dc5709ce7 | ||
|
|
ccb09fbf45 | ||
|
|
fb883254f6 | ||
|
|
5f1f3dff5e | ||
|
|
dd93de75d9 | ||
|
|
d897159560 | ||
|
|
182c975e01 | ||
|
|
f19a324030 | ||
|
|
9cf965a157 | ||
|
|
9e42be09d7 | ||
|
|
7d466be94e | ||
|
|
a5c0cbc3b4 | ||
|
|
bb5dc4c473 | ||
|
|
132386f64d | ||
|
|
b0c89d739b | ||
|
|
6a07a7ed2c | ||
|
|
f293ab86c3 | ||
|
|
db60abc604 | ||
|
|
f55c93a1e4 | ||
|
|
08296bd3ce | ||
|
|
fca6da3a5a | ||
|
|
49407c2e4f | ||
|
|
4d1474afe0 | ||
|
|
aa128d6c07 | ||
|
|
c8c76cc03f | ||
|
|
5a9bfea419 | ||
|
|
dffe70e40a | ||
|
|
1b4d9a6147 | ||
|
|
b7fb3ec9a9 | ||
|
|
7ff18ca14b | ||
|
|
6052b1546d | ||
|
|
5a241392cf | ||
|
|
c586c63533 | ||
|
|
f8919f8baa | ||
|
|
f45ed1613e | ||
|
|
bc665fdbc3 | ||
|
|
6c3e6bc90c | ||
|
|
cb3b7f3cb2 | ||
|
|
5e8db32329 | ||
|
|
65c39763eb | ||
|
|
f429469fc8 | ||
|
|
6339b62f8d | ||
|
|
473e9ab55c | ||
|
|
77e503f5ca | ||
|
|
c64ecb1ab8 | ||
|
|
2e22dcfc47 | ||
|
|
16fd6558a6 | ||
|
|
eb97151476 | ||
|
|
b8adee4004 | ||
|
|
65f5eacdd4 | ||
|
|
2b6597e9f1 | ||
|
|
6a2f73df9c | ||
|
|
60a03d0293 | ||
|
|
dd6735285d | ||
|
|
dbf5c395b0 | ||
|
|
8261ffeeeb | ||
|
|
1e438cdc45 | ||
|
|
d70859ef1b | ||
|
|
3ab8ff2ea1 | ||
|
|
adea414695 | ||
|
|
cace03c3cc | ||
|
|
d55b1d7259 | ||
|
|
78e963688c | ||
|
|
b2df872ad4 | ||
|
|
fea4201f46 | ||
|
|
073a2f4d65 | ||
|
|
06df678d33 | ||
|
|
dd65c231f9 | ||
|
|
ae690e0679 | ||
|
|
06f108df3b | ||
|
|
a8319d1131 | ||
|
|
f800290d89 | ||
|
|
e79de47f5e | ||
|
|
08c22416a2 | ||
|
|
dac5d313b3 | ||
|
|
21d139c6c2 | ||
|
|
e5d997a6c0 | ||
|
|
a2e54dc94f | ||
|
|
28040109ac | ||
|
|
39b6bf62ba | ||
|
|
6a19a8fb7e | ||
|
|
e914b6fe8d | ||
|
|
e5a6dbd77a | ||
|
|
343dc05f82 | ||
|
|
76076cdb3c | ||
|
|
487539542a | ||
|
|
1634bef28a | ||
|
|
6882f7a9c0 | ||
|
|
943c8507f7 | ||
|
|
99119562bb | ||
|
|
5cbe595fe3 | ||
|
|
eb8257a52e | ||
|
|
27d8afe4a7 | ||
|
|
3391ec5377 | ||
|
|
88f21eae87 | ||
|
|
1258923a0d | ||
|
|
344286e037 | ||
|
|
3ced81a2c2 | ||
|
|
032c48fb25 | ||
|
|
f6125a2542 | ||
|
|
49a03a86fb | ||
|
|
e84acd9898 | ||
|
|
8022a74565 | ||
|
|
fdc6fc59b8 | ||
|
|
f3340b113f | ||
|
|
20f24bffc4 | ||
|
|
7ebcc69cb9 | ||
|
|
90e5c7f3eb | ||
|
|
81918713dd | ||
|
|
9505925363 | ||
|
|
6e123bcea2 | ||
|
|
ddceaaf3d5 | ||
|
|
a3fb0a5ac5 | ||
|
|
1ef5379648 | ||
|
|
5ec50461ee | ||
|
|
e960642399 | ||
|
|
2bab2acf5b | ||
|
|
5a56a822b0 | ||
|
|
35154db50f | ||
|
|
fe2ae6ec68 | ||
|
|
b249a48dcf | ||
|
|
035ebc881a | ||
|
|
8b5fd2b4ac | ||
|
|
e813fcd9c8 | ||
|
|
634e7170e3 | ||
|
|
b6a0303a4c | ||
|
|
824b900a43 | ||
|
|
49a83db2e9 | ||
|
|
af7e42d640 | ||
|
|
9183cba02e | ||
|
|
c3d297dd05 | ||
|
|
527d6ca4d8 | ||
|
|
5e4d173f1d | ||
|
|
c3c1a0fb4e | ||
|
|
7b8c691402 | ||
|
|
a1e7351072 | ||
|
|
7efa45126c | ||
|
|
c12cd7fd57 | ||
|
|
4ddff9ee16 | ||
|
|
615cb24cce | ||
|
|
3bef6d5066 | ||
|
|
e8a07c9a6c | ||
|
|
3041499cd4 | ||
|
|
03590f117e | ||
|
|
c1311e4619 | ||
|
|
9a94004fc9 | ||
|
|
095fec95e5 | ||
|
|
b78f0aa9db | ||
|
|
fac3118b0a | ||
|
|
cf056b3464 | ||
|
|
9ed221b168 | ||
|
|
e12752f9ac | ||
|
|
70b3f6bd92 | ||
|
|
5022015e4a | ||
|
|
f41a7000de | ||
|
|
a67da7bc59 | ||
|
|
943bd9e48e | ||
|
|
2b14469a58 | ||
|
|
2be6dec238 | ||
|
|
e1f5245145 | ||
|
|
b3513e3203 | ||
|
|
ffd9d957f4 | ||
|
|
3cea54e3a6 | ||
|
|
ef4223858b | ||
|
|
5514812a4d | ||
|
|
87abe5e648 | ||
|
|
7e3703b2bf | ||
|
|
f652b13520 | ||
|
|
baf6b96db6 | ||
|
|
701c318b60 | ||
|
|
b3b81d6a76 | ||
|
|
8a5428808e | ||
|
|
a939c45d58 | ||
|
|
0b2fe75828 | ||
|
|
28d0ab9da8 | ||
|
|
103a3c4ca7 | ||
|
|
5c52da80c6 | ||
|
|
8fe10b3331 | ||
|
|
04a72b9608 | ||
|
|
7280234422 | ||
|
|
4078e84fb6 | ||
|
|
06f32a47ec | ||
|
|
f4a5e49b63 | ||
|
|
5d74b776d6 | ||
|
|
5cae1bb134 | ||
|
|
8d3dc790df | ||
|
|
f5bacb79c1 | ||
|
|
4a425c2674 | ||
|
|
c38b6d585e | ||
|
|
ad5f3fefc5 | ||
|
|
707b80f436 | ||
|
|
80feb67fc2 | ||
|
|
aae63de755 | ||
|
|
e9e376419d | ||
|
|
ffeb0420bf | ||
|
|
a63bb44ba2 | ||
|
|
91135c95ae | ||
|
|
bff7831912 | ||
|
|
66186273ca | ||
|
|
07fea02146 | ||
|
|
593f57fd2c | ||
|
|
d40e9bd3c1 | ||
|
|
92aba77cc1 | ||
|
|
e4e14cd9f4 | ||
|
|
327a159a10 | ||
|
|
d57fb445eb | ||
|
|
fe98c30077 | ||
|
|
758ae41999 | ||
|
|
67229912e6 | ||
|
|
51c593b8cd | ||
|
|
39f93dfa33 | ||
|
|
3559c5dc3c | ||
|
|
bb3c76a9d9 | ||
|
|
ca15db81bb | ||
|
|
f000cedcbb | ||
|
|
681e3d751e | ||
|
|
66507cbf91 | ||
|
|
f11c24e359 | ||
|
|
3ade41a76d | ||
|
|
8926837a3e | ||
|
|
35b9fe043c | ||
|
|
82ba2b1b0d | ||
|
|
57f57f5c75 | ||
|
|
08896ee9c9 | ||
|
|
917ba90f2c | ||
|
|
3cf104580e | ||
|
|
4bba11cd94 | ||
|
|
7cfe6addf0 | ||
|
|
e4781b8aa3 | ||
|
|
9de8e79577 | ||
|
|
3826f6fae2 | ||
|
|
711bf244ed | ||
|
|
99d0811412 | ||
|
|
ee2d28d589 | ||
|
|
22e32117e0 | ||
|
|
fed1b62c5d | ||
|
|
5b1e65c23e | ||
|
|
e72756d01a | ||
|
|
871ecf83fb | ||
|
|
daaa35bc37 | ||
|
|
9d10ccef70 | ||
|
|
2aa71d1737 | ||
|
|
b9b1102b74 | ||
|
|
f824582aac | ||
|
|
199f289ee3 | ||
|
|
668b67dcdb | ||
|
|
190771ddf1 | ||
|
|
05dcc6e3af | ||
|
|
6f7200868b | ||
|
|
62b70b561e | ||
|
|
1f13427dee | ||
|
|
6aee573e2e | ||
|
|
c5c285abc3 | ||
|
|
0a4fd79b22 | ||
|
|
b29edc7056 | ||
|
|
97aa3f5f36 | ||
|
|
d66c33a8b9 | ||
|
|
63383f8032 | ||
|
|
9599003bc3 | ||
|
|
a63a01c9ea | ||
|
|
dabf4e8bbd | ||
|
|
35ffdb4745 | ||
|
|
b0d63980e3 | ||
|
|
c27ffbda8b | ||
|
|
502fc62967 | ||
|
|
c06ac70633 | ||
|
|
414d51b02b | ||
|
|
4b6b45cf43 | ||
|
|
986f39838c | ||
|
|
d90affa47d | ||
|
|
d5a075da30 | ||
|
|
1b06194455 | ||
|
|
e2bec73011 | ||
|
|
07613bcca0 | ||
|
|
cc24484eaf | ||
|
|
8549bd70b7 | ||
|
|
9bd2e70376 | ||
|
|
569f26776e | ||
|
|
7f21cdd1f4 | ||
|
|
7b5a0c6dcf | ||
|
|
1e1de85685 | ||
|
|
9b1275f182 | ||
|
|
cb531056a6 | ||
|
|
b0200d462d | ||
|
|
028e78f46b | ||
|
|
9ca7b22cec | ||
|
|
1b45ab2601 | ||
|
|
215bbaac6c | ||
|
|
53f96099fb | ||
|
|
681256a079 | ||
|
|
2a4663c940 | ||
|
|
1d88d061e6 | ||
|
|
123c6dffe0 | ||
|
|
91bdc4bde2 | ||
|
|
190ad06f1a | ||
|
|
f8022a5c2f | ||
|
|
e52380915b | ||
|
|
08bf19d4aa | ||
|
|
a2cc7bd4b9 | ||
|
|
b8637bf1fa | ||
|
|
417c31bc0b | ||
|
|
1f20c03afa | ||
|
|
d70e4740fc | ||
|
|
b1d42381cd | ||
|
|
aa089980ce | ||
|
|
52acd959e0 | ||
|
|
8cf65bc37d | ||
|
|
24132c8f5b | ||
|
|
fe1c447d9a | ||
|
|
bde9210fa3 | ||
|
|
10e31c397c | ||
|
|
e2ad59a74d | ||
|
|
f604449188 | ||
|
|
f3b965d466 | ||
|
|
e7cdb8ad54 | ||
|
|
898347366d | ||
|
|
9ef1f1b73c | ||
|
|
eea6eb6263 | ||
|
|
548ab4f78c | ||
|
|
b464dc15f2 | ||
|
|
8112c5d3f2 | ||
|
|
eb326cd1bb | ||
|
|
208ecbc3f7 | ||
|
|
61d5425fdf | ||
|
|
d590600c12 | ||
|
|
12a2f23101 | ||
|
|
f351db608e | ||
|
|
2c59d948fc | ||
|
|
72199cc0b3 | ||
|
|
fc463ae50b | ||
|
|
86a8addf49 | ||
|
|
b367e94261 | ||
|
|
df68ca8dcf | ||
|
|
9b63cd35f0 | ||
|
|
84ea3f8cb1 | ||
|
|
5d5373454c | ||
|
|
a3d6917f20 | ||
|
|
87ee15a081 | ||
|
|
f44cda2621 | ||
|
|
dd1e5b5c0f | ||
|
|
adb6c7789c | ||
|
|
d6788a0839 | ||
|
|
2b65adc15f | ||
|
|
b91998a0d8 | ||
|
|
505cf5b251 | ||
|
|
5ea4ef9e55 | ||
|
|
e8de4655ac | ||
|
|
76657d5239 | ||
|
|
d63040283f | ||
|
|
04c2ad9430 | ||
|
|
75f040f38e | ||
|
|
0e69dc6885 | ||
|
|
f1e7d3d6da | ||
|
|
c07b60d527 | ||
|
|
2743e4a182 | ||
|
|
972c3dd36b | ||
|
|
9ccef223f6 | ||
|
|
fc52ff65bd | ||
|
|
8325151e16 | ||
|
|
b3766576d7 | ||
|
|
e3153295a9 | ||
|
|
c8c86d2bad | ||
|
|
3420293f36 | ||
|
|
0370cb64e7 | ||
|
|
a50b94be0c | ||
|
|
62a9d4ea91 | ||
|
|
c2fe500eb8 | ||
|
|
340afb2a50 | ||
|
|
3873ab811a | ||
|
|
4ab856a0be | ||
|
|
46ebff2163 | ||
|
|
afa26f7d3c | ||
|
|
9382439a05 | ||
|
|
8495a7c05a | ||
|
|
019c3c9ef9 | ||
|
|
77334af34e | ||
|
|
8f09efab9d | ||
|
|
c34db4cf01 | ||
|
|
6054b69eec | ||
|
|
6b8ec0bb82 | ||
|
|
a0d275c850 | ||
|
|
179e371d0e | ||
|
|
26411a123e | ||
|
|
126dd70efc | ||
|
|
461c62ea10 | ||
|
|
bda57d00df | ||
|
|
bd3eb9d662 | ||
|
|
a571781240 | ||
|
|
a5f675d693 | ||
|
|
4a044fe867 | ||
|
|
0fc7fa557d | ||
|
|
79f9de9de4 | ||
|
|
4bf23cc83a | ||
|
|
82815ff614 | ||
|
|
bdfab04c10 | ||
|
|
2a2f255640 | ||
|
|
1d8e38f0c6 | ||
|
|
dd4c0d448c | ||
|
|
6b84b9b4b6 | ||
|
|
8ac7d1deca | ||
|
|
f0511c5f51 | ||
|
|
502627f590 | ||
|
|
806b554fb2 | ||
|
|
13261b52db | ||
|
|
a21a94078b | ||
|
|
b0fbe5c8ba | ||
|
|
ddad1cb8af | ||
|
|
8a17b61f4e | ||
|
|
7c66f76858 | ||
|
|
3d12bf7d14 | ||
|
|
851d0192ad | ||
|
|
42759be6ff | ||
|
|
0f1644e612 | ||
|
|
64b75d2806 | ||
|
|
4bfb62d7f4 | ||
|
|
6369c26671 | ||
|
|
71e0b00600 | ||
|
|
be65c14a6a | ||
|
|
7b2e72d395 | ||
|
|
a9297df89c | ||
|
|
4ba72e2d2d | ||
|
|
5f49db6c7f | ||
|
|
fc3c07f6de | ||
|
|
c7cba6d5ad | ||
|
|
6f624b29e4 | ||
|
|
f762173eb0 | ||
|
|
35ada9d636 | ||
|
|
81a4ba854c | ||
|
|
5758c7e175 | ||
|
|
ba2f8cdeef | ||
|
|
c2fa2de290 | ||
|
|
932aef887c | ||
|
|
bae4d4c673 | ||
|
|
6bf050e05c | ||
|
|
79349e6307 | ||
|
|
a77be7eb7b | ||
|
|
082714a436 | ||
|
|
06bb6f00e5 | ||
|
|
e4c875eb41 | ||
|
|
f0c1e65b2d | ||
|
|
b5fc6045fd | ||
|
|
d99dcc0d4b | ||
|
|
56750cba83 | ||
|
|
6e3303145f | ||
|
|
d8c0cc447f | ||
|
|
3ee999ae2a | ||
|
|
0bedbb4dd3 |
No files matched your search
+10
@@ -22,6 +22,16 @@ jdk:
|
|||||||
|
|
||||||
install: true
|
install: true
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- "export PHANTOMJS_VERSION=2.1.1"
|
||||||
|
- "phantomjs --version"
|
||||||
|
- "export PATH=$PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64/bin:$PATH"
|
||||||
|
- "phantomjs --version"
|
||||||
|
- "if [ $(phantomjs --version) != '$PHANTOMJS_VERSION' ]; then rm -rf $PWD/travis_phantomjs; mkdir -p $PWD/travis_phantomjs; fi"
|
||||||
|
- "if [ $(phantomjs --version) != '$PHANTOMJS_VERSION' ]; then wget https://github.com/Medium/phantomjs/releases/download/v$PHANTOMJS_VERSION/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -O $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2; fi"
|
||||||
|
- "if [ $(phantomjs --version) != '$PHANTOMJS_VERSION' ]; then tar -xvf $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -C $PWD/travis_phantomjs; fi"
|
||||||
|
- "phantomjs --version"
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- ./travis-run-tests.sh $TESTS
|
- ./travis-run-tests.sh $TESTS
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+8
@@ -102,6 +102,14 @@ public class AuthenticatedActionsHandler {
|
|||||||
KeycloakSecurityContext securityContext = facade.getSecurityContext();
|
KeycloakSecurityContext securityContext = facade.getSecurityContext();
|
||||||
String origin = facade.getRequest().getHeader(CorsHeaders.ORIGIN);
|
String origin = facade.getRequest().getHeader(CorsHeaders.ORIGIN);
|
||||||
String exposeHeaders = deployment.getCorsExposedHeaders();
|
String exposeHeaders = deployment.getCorsExposedHeaders();
|
||||||
|
|
||||||
|
if (deployment.getPolicyEnforcer() != null) {
|
||||||
|
if (exposeHeaders != null) {
|
||||||
|
exposeHeaders += ",";
|
||||||
|
}
|
||||||
|
exposeHeaders += "WWW-Authenticate";
|
||||||
|
}
|
||||||
|
|
||||||
String requestOrigin = UriUtils.getOrigin(facade.getRequest().getURI());
|
String requestOrigin = UriUtils.getOrigin(facade.getRequest().getURI());
|
||||||
log.debugv("Origin: {0} uri: {1}", origin, facade.getRequest().getURI());
|
log.debugv("Origin: {0} uri: {1}", origin, facade.getRequest().getURI());
|
||||||
if (securityContext != null && origin != null && !origin.equals(requestOrigin)) {
|
if (securityContext != null && origin != null && !origin.equals(requestOrigin)) {
|
||||||
|
|||||||
+4
@@ -174,6 +174,10 @@ public class BearerTokenRequestAuthenticator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean challenge(HttpFacade facade) {
|
public boolean challenge(HttpFacade facade) {
|
||||||
|
if (deployment.getPolicyEnforcer() != null) {
|
||||||
|
deployment.getPolicyEnforcer().enforce(OIDCHttpFacade.class.cast(facade));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
OIDCAuthenticationError error = new OIDCAuthenticationError(reason, description);
|
OIDCAuthenticationError error = new OIDCAuthenticationError(reason, description);
|
||||||
facade.getRequest().setError(error);
|
facade.getRequest().setError(error);
|
||||||
facade.getResponse().addHeader("WWW-Authenticate", challenge);
|
facade.getResponse().addHeader("WWW-Authenticate", challenge);
|
||||||
|
|||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
package org.keycloak.adapters.authentication;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.adapters.AdapterUtils;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.jose.jws.JWSBuilder;
|
||||||
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
|
import org.keycloak.representations.JsonWebToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client authentication based on JWT signed by client secret instead of private key .
|
||||||
|
* See <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">specs</a> for more details.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public class JWTClientSecretCredentialsProvider implements ClientCredentialsProvider {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(JWTClientSecretCredentialsProvider.class);
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = "secret-jwt";
|
||||||
|
|
||||||
|
private SecretKey clientSecret;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(KeycloakDeployment deployment, Object config) {
|
||||||
|
if (config == null || !(config instanceof Map)) {
|
||||||
|
throw new RuntimeException("Configuration of jwt credentials by client secret is missing or incorrect for client '" + deployment.getResourceName() + "'. Check your adapter configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> cfg = (Map<String, Object>) config;
|
||||||
|
String clientSecretString = (String) cfg.get("secret");
|
||||||
|
if (clientSecretString == null) {
|
||||||
|
throw new RuntimeException("Missing parameter secret-jwt in configuration of jwt for client " + deployment.getResourceName());
|
||||||
|
}
|
||||||
|
setClientSecret(clientSecretString);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams) {
|
||||||
|
String signedToken = createSignedRequestToken(deployment.getResourceName(), deployment.getRealmInfoUrl());
|
||||||
|
formParams.put(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT);
|
||||||
|
formParams.put(OAuth2Constants.CLIENT_ASSERTION, signedToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientSecret(String clientSecretString) {
|
||||||
|
// Get client secret and validate signature
|
||||||
|
// According to <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OIDC's client authentication spec</a>,
|
||||||
|
// The HMAC (Hash-based Message Authentication Code) is calculated using the octets of the UTF-8 representation of the client_secret as the shared key.
|
||||||
|
// Use "HmacSHA256" consulting <a href="https://docs.oracle.com/javase/jp/8/docs/api/javax/crypto/Mac.html">java8 api</a>
|
||||||
|
// because it must be implemented in every java platform.
|
||||||
|
try {
|
||||||
|
clientSecret = new SecretKeySpec(clientSecretString.getBytes("UTF-8"), "HmacSHA256");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("Failed to create secret key spec due to unsupported encoding.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String createSignedRequestToken(String clientId, String realmInfoUrl) {
|
||||||
|
JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl);
|
||||||
|
// JOSE header {"alg":"HS256","typ" : "JWT"} no need "kid" due to using only one registered client secret.
|
||||||
|
// Use "HmacSHA256" consulting <a href="https://docs.oracle.com/javase/jp/8/docs/api/javax/crypto/Mac.html">java8 api</a>.
|
||||||
|
// because it must be implemented in every java platform.
|
||||||
|
return new JWSBuilder().jsonContent(jwt).hmac256(clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
|
||||||
|
// According to <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OIDC's client authentication spec</a>,
|
||||||
|
// JWT claims is the same as one by private_key_jwt
|
||||||
|
|
||||||
|
JsonWebToken reqToken = new JsonWebToken();
|
||||||
|
reqToken.id(AdapterUtils.generateId());
|
||||||
|
reqToken.issuer(clientId);
|
||||||
|
reqToken.subject(clientId);
|
||||||
|
reqToken.audience(realmInfoUrl);
|
||||||
|
|
||||||
|
int now = Time.currentTime();
|
||||||
|
reqToken.issuedAt(now);
|
||||||
|
// the same as in KEYCLOAK-2986, JWTClientCredentialsProvider's timeout field
|
||||||
|
reqToken.expiration(now + 10);
|
||||||
|
reqToken.notBefore(now);
|
||||||
|
return reqToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+138
-78
@@ -18,23 +18,27 @@
|
|||||||
package org.keycloak.adapters.authorization;
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.AuthorizationContext;
|
import org.keycloak.AuthorizationContext;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
import org.keycloak.adapters.OIDCHttpFacade;
|
import org.keycloak.adapters.OIDCHttpFacade;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
import org.keycloak.adapters.spi.HttpFacade.Request;
|
import org.keycloak.adapters.spi.HttpFacade.Request;
|
||||||
import org.keycloak.authorization.client.AuthzClient;
|
import org.keycloak.authorization.client.AuthzClient;
|
||||||
import org.keycloak.authorization.client.ClientAuthorizationContext;
|
import org.keycloak.authorization.client.ClientAuthorizationContext;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.AccessToken.Authorization;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||||
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.ScopeEnforcementMode;
|
||||||
import org.keycloak.representations.idm.authorization.Permission;
|
import org.keycloak.representations.idm.authorization.Permission;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,76 +47,84 @@ import org.keycloak.representations.idm.authorization.Permission;
|
|||||||
public abstract class AbstractPolicyEnforcer {
|
public abstract class AbstractPolicyEnforcer {
|
||||||
|
|
||||||
private static Logger LOGGER = Logger.getLogger(AbstractPolicyEnforcer.class);
|
private static Logger LOGGER = Logger.getLogger(AbstractPolicyEnforcer.class);
|
||||||
private final PolicyEnforcerConfig enforcerConfig;
|
private static final String HTTP_METHOD_DELETE = "DELETE";
|
||||||
|
|
||||||
private final PolicyEnforcer policyEnforcer;
|
private final PolicyEnforcer policyEnforcer;
|
||||||
|
|
||||||
private Map<String, PathConfig> paths;
|
protected AbstractPolicyEnforcer(PolicyEnforcer policyEnforcer) {
|
||||||
private AuthzClient authzClient;
|
|
||||||
private PathMatcher pathMatcher;
|
|
||||||
|
|
||||||
public AbstractPolicyEnforcer(PolicyEnforcer policyEnforcer) {
|
|
||||||
this.policyEnforcer = policyEnforcer;
|
this.policyEnforcer = policyEnforcer;
|
||||||
this.enforcerConfig = policyEnforcer.getEnforcerConfig();
|
|
||||||
this.authzClient = policyEnforcer.getClient();
|
|
||||||
this.pathMatcher = policyEnforcer.getPathMatcher();
|
|
||||||
this.paths = policyEnforcer.getPaths();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthorizationContext authorize(OIDCHttpFacade httpFacade) {
|
public AuthorizationContext authorize(OIDCHttpFacade httpFacade) {
|
||||||
EnforcementMode enforcementMode = this.enforcerConfig.getEnforcementMode();
|
EnforcementMode enforcementMode = getEnforcerConfig().getEnforcementMode();
|
||||||
|
|
||||||
if (EnforcementMode.DISABLED.equals(enforcementMode)) {
|
if (EnforcementMode.DISABLED.equals(enforcementMode)) {
|
||||||
return createEmptyAuthorizationContext(true);
|
return createEmptyAuthorizationContext(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Request request = httpFacade.getRequest();
|
||||||
|
PathConfig pathConfig = getPathConfig(request);
|
||||||
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
|
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
|
||||||
|
|
||||||
if (securityContext != null) {
|
if (securityContext == null) {
|
||||||
AccessToken accessToken = securityContext.getToken();
|
if (pathConfig != null) {
|
||||||
|
challenge(pathConfig, getRequiredScopes(pathConfig, request), httpFacade);
|
||||||
|
}
|
||||||
|
return createEmptyAuthorizationContext(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (accessToken != null) {
|
AccessToken accessToken = securityContext.getToken();
|
||||||
Request request = httpFacade.getRequest();
|
|
||||||
String path = getPath(request);
|
|
||||||
PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
|
|
||||||
|
|
||||||
|
if (accessToken != null) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
|
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
|
||||||
|
}
|
||||||
|
|
||||||
if (pathConfig == null) {
|
if (pathConfig == null) {
|
||||||
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
|
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
|
||||||
return createAuthorizationContext(accessToken, null);
|
return createAuthorizationContext(accessToken, null);
|
||||||
}
|
|
||||||
|
|
||||||
LOGGER.debugf("Could not find a configuration for path [%s]", path);
|
|
||||||
|
|
||||||
if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
|
|
||||||
return createAuthorizationContext(accessToken, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAccessDenied(httpFacade);
|
|
||||||
|
|
||||||
return createEmptyAuthorizationContext(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
|
if (LOGGER.isDebugEnabled()) {
|
||||||
return createEmptyAuthorizationContext(true);
|
LOGGER.debugf("Could not find a configuration for path [%s]", getPath(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
MethodConfig methodConfig = getRequiredScopes(pathConfig, request);
|
if (isDefaultAccessDeniedUri(request)) {
|
||||||
|
return createAuthorizationContext(accessToken, null);
|
||||||
if (isAuthorized(pathConfig, methodConfig, accessToken, httpFacade)) {
|
|
||||||
try {
|
|
||||||
return createAuthorizationContext(accessToken, pathConfig);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleAccessDenied(httpFacade);
|
||||||
|
|
||||||
|
return createEmptyAuthorizationContext(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
|
||||||
|
return createAuthorizationContext(accessToken, pathConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodConfig methodConfig = getRequiredScopes(pathConfig, request);
|
||||||
|
|
||||||
|
if (isAuthorized(pathConfig, methodConfig, accessToken, httpFacade)) {
|
||||||
|
try {
|
||||||
|
return createAuthorizationContext(accessToken, pathConfig);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (methodConfig != null && ScopeEnforcementMode.DISABLED.equals(methodConfig.getScopesEnforcementMode())) {
|
||||||
|
return createEmptyAuthorizationContext(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
|
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
|
||||||
|
}
|
||||||
|
|
||||||
if (!challenge(pathConfig, methodConfig, httpFacade)) {
|
if (!challenge(pathConfig, methodConfig, httpFacade)) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
|
LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
|
||||||
handleAccessDenied(httpFacade);
|
|
||||||
}
|
}
|
||||||
|
handleAccessDenied(httpFacade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,23 +135,22 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
|
|
||||||
protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade) {
|
protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade) {
|
||||||
Request request = httpFacade.getRequest();
|
Request request = httpFacade.getRequest();
|
||||||
PolicyEnforcerConfig enforcerConfig = getEnforcerConfig();
|
|
||||||
|
|
||||||
if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
|
if (isDefaultAccessDeniedUri(request)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
AccessToken.Authorization authorization = accessToken.getAuthorization();
|
Authorization authorization = accessToken.getAuthorization();
|
||||||
|
|
||||||
if (authorization == null) {
|
if (authorization == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Permission> permissions = authorization.getPermissions();
|
|
||||||
boolean hasPermission = false;
|
boolean hasPermission = false;
|
||||||
|
List<Permission> grantedPermissions = authorization.getPermissions();
|
||||||
|
|
||||||
for (Permission permission : permissions) {
|
for (Permission permission : grantedPermissions) {
|
||||||
if (permission.getResourceSetId() != null) {
|
if (permission.getResourceId() != null) {
|
||||||
if (isResourcePermission(actualPathConfig, permission)) {
|
if (isResourcePermission(actualPathConfig, permission)) {
|
||||||
hasPermission = true;
|
hasPermission = true;
|
||||||
|
|
||||||
@@ -148,11 +159,14 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasResourceScopePermission(methodConfig, permission)) {
|
if (hasResourceScopePermission(methodConfig, permission)) {
|
||||||
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
|
if (LOGGER.isDebugEnabled()) {
|
||||||
if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) {
|
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, grantedPermissions);
|
||||||
this.paths.remove(actualPathConfig);
|
|
||||||
}
|
}
|
||||||
return true;
|
if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) {
|
||||||
|
policyEnforcer.getPathMatcher().removeFromCache(getPath(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasValidClaims(actualPathConfig, permission, httpFacade, authorization);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -167,24 +181,54 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, permissions);
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, grantedPermissions);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean hasValidClaims(PathConfig actualPathConfig, Permission permission, OIDCHttpFacade httpFacade, Authorization authorization) {
|
||||||
|
Map<String, Set<String>> grantedClaims = permission.getClaims();
|
||||||
|
|
||||||
|
if (grantedClaims != null) {
|
||||||
|
Map<String, List<String>> claims = resolveClaims(actualPathConfig, httpFacade);
|
||||||
|
|
||||||
|
if (claims.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Entry<String, Set<String>> entry : grantedClaims.entrySet()) {
|
||||||
|
List<String> requestClaims = claims.get(entry.getKey());
|
||||||
|
|
||||||
|
if (requestClaims == null || requestClaims.isEmpty() || !entry.getValue().containsAll(requestClaims)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
protected void handleAccessDenied(OIDCHttpFacade httpFacade) {
|
protected void handleAccessDenied(OIDCHttpFacade httpFacade) {
|
||||||
httpFacade.getResponse().sendError(403);
|
httpFacade.getResponse().sendError(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isDefaultAccessDeniedUri(Request request, PolicyEnforcerConfig enforcerConfig) {
|
protected AuthzClient getAuthzClient() {
|
||||||
String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
|
return policyEnforcer.getClient();
|
||||||
|
}
|
||||||
|
|
||||||
if (accessDeniedPath != null) {
|
protected PolicyEnforcerConfig getEnforcerConfig() {
|
||||||
if (request.getURI().contains(accessDeniedPath)) {
|
return policyEnforcer.getEnforcerConfig();
|
||||||
return true;
|
}
|
||||||
}
|
|
||||||
}
|
protected PolicyEnforcer getPolicyEnforcer() {
|
||||||
return false;
|
return policyEnforcer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDefaultAccessDeniedUri(Request request) {
|
||||||
|
String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo();
|
||||||
|
return accessDeniedPath != null && request.getURI().contains(accessDeniedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) {
|
private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) {
|
||||||
@@ -212,20 +256,8 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
return requiredScopes.isEmpty();
|
return requiredScopes.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected AuthzClient getAuthzClient() {
|
|
||||||
return this.authzClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected PolicyEnforcerConfig getEnforcerConfig() {
|
|
||||||
return enforcerConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected PolicyEnforcer getPolicyEnforcer() {
|
|
||||||
return policyEnforcer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) {
|
private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) {
|
||||||
return new ClientAuthorizationContext(authzClient) {
|
return new ClientAuthorizationContext(getAuthzClient()) {
|
||||||
@Override
|
@Override
|
||||||
public boolean hasPermission(String resourceName, String scopeName) {
|
public boolean hasPermission(String resourceName, String scopeName) {
|
||||||
return granted;
|
return granted;
|
||||||
@@ -276,7 +308,7 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {
|
private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {
|
||||||
return new ClientAuthorizationContext(accessToken, pathConfig, this.paths, authzClient);
|
return new ClientAuthorizationContext(accessToken, pathConfig, getAuthzClient());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
|
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
|
||||||
@@ -292,6 +324,34 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) {
|
private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) {
|
||||||
return permission.getResourceSetId().equals(actualPathConfig.getId());
|
return permission.getResourceId().equals(actualPathConfig.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private PathConfig getPathConfig(Request request) {
|
||||||
|
return isDefaultAccessDeniedUri(request) ? null : policyEnforcer.getPathMatcher().matches(getPath(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Map<String, List<String>> resolveClaims(PathConfig pathConfig, OIDCHttpFacade httpFacade) {
|
||||||
|
Map<String, List<String>> claims = getClaims(getEnforcerConfig().getClaimInformationPointConfig(), httpFacade);
|
||||||
|
|
||||||
|
claims.putAll(getClaims(pathConfig.getClaimInformationPointConfig(), httpFacade));
|
||||||
|
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, List<String>> getClaims(Map<String, Map<String, Object>>claimInformationPointConfig, HttpFacade httpFacade) {
|
||||||
|
Map<String, List<String>> claims = new HashMap<>();
|
||||||
|
|
||||||
|
if (claimInformationPointConfig != null) {
|
||||||
|
for (Entry<String, Map<String, Object>> claimDef : claimInformationPointConfig.entrySet()) {
|
||||||
|
ClaimInformationPointProviderFactory factory = getPolicyEnforcer().getClaimInformationPointProviderFactories().get(claimDef.getKey());
|
||||||
|
|
||||||
|
if (factory != null) {
|
||||||
|
claims.putAll(factory.create(claimDef.getValue()).resolve(httpFacade));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-86
@@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
|
||||||
* and other contributors as indicated by the @author tags.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package org.keycloak.adapters.authorization;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
import org.keycloak.adapters.OIDCHttpFacade;
|
|
||||||
import org.keycloak.adapters.spi.HttpFacade;
|
|
||||||
import org.keycloak.authorization.client.AuthzClient;
|
|
||||||
import org.keycloak.authorization.client.representation.PermissionRequest;
|
|
||||||
import org.keycloak.authorization.client.resource.PermissionResource;
|
|
||||||
import org.keycloak.authorization.client.resource.ProtectionResource;
|
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
|
||||||
*/
|
|
||||||
public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer {
|
|
||||||
|
|
||||||
private static Logger LOGGER = Logger.getLogger(BearerTokenPolicyEnforcer.class);
|
|
||||||
|
|
||||||
public BearerTokenPolicyEnforcer(PolicyEnforcer enforcer) {
|
|
||||||
super(enforcer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
|
|
||||||
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
|
||||||
challengeUmaAuthentication(pathConfig, methodConfig, facade);
|
|
||||||
} else {
|
|
||||||
challengeEntitlementAuthentication(facade);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void challengeEntitlementAuthentication(OIDCHttpFacade facade) {
|
|
||||||
HttpFacade.Response response = facade.getResponse();
|
|
||||||
AuthzClient authzClient = getAuthzClient();
|
|
||||||
String clientId = authzClient.getConfiguration().getResource();
|
|
||||||
String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/entitlement";
|
|
||||||
response.setStatus(401);
|
|
||||||
response.setHeader("WWW-Authenticate", "KC_ETT realm=\"" + clientId + "\",as_uri=\"" + authorizationServerUri + "\"");
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
|
||||||
LOGGER.debug("Sending Entitlement challenge");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void challengeUmaAuthentication(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
|
|
||||||
HttpFacade.Response response = facade.getResponse();
|
|
||||||
AuthzClient authzClient = getAuthzClient();
|
|
||||||
String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient);
|
|
||||||
String clientId = authzClient.getConfiguration().getResource();
|
|
||||||
String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/authorize";
|
|
||||||
response.setStatus(401);
|
|
||||||
response.setHeader("WWW-Authenticate", "UMA realm=\"" + clientId + "\",as_uri=\"" + authorizationServerUri + "\",ticket=\"" + ticket + "\"");
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
|
||||||
LOGGER.debug("Sending UMA challenge");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getPermissionTicket(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AuthzClient authzClient) {
|
|
||||||
ProtectionResource protection = authzClient.protection();
|
|
||||||
PermissionResource permission = protection.permission();
|
|
||||||
PermissionRequest permissionRequest = new PermissionRequest();
|
|
||||||
permissionRequest.setResourceSetId(pathConfig.getId());
|
|
||||||
permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
|
|
||||||
return permission.forResource(permissionRequest).getTicket();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public interface ClaimInformationPointProvider {
|
||||||
|
|
||||||
|
Map<String, List<String>> resolve(HttpFacade httpFacade);
|
||||||
|
}
|
||||||
+11
-8
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
* and other contributors as indicated by the @author tags.
|
* and other contributors as indicated by the @author tags.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@@ -14,17 +14,20 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.adapter.example.authorization;
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
import org.jboss.arquillian.container.test.api.RunAsClient;
|
import java.util.Map;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
|
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
@RunAsClient
|
public interface ClaimInformationPointProviderFactory<C extends ClaimInformationPointProvider> {
|
||||||
@AppServerContainer("app-server-wildfly")
|
|
||||||
//@AdapterLibsLocationProperty("adapter.libs.wildfly")
|
|
||||||
public class WildflyServletPolicyEnforcerAdapterTest extends AbstractServletPolicyEnforcerTest {
|
|
||||||
|
|
||||||
|
String getName();
|
||||||
|
|
||||||
|
void init(PolicyEnforcer policyEnforcer);
|
||||||
|
|
||||||
|
C create(Map<String, Object> config);
|
||||||
}
|
}
|
||||||
+127
-52
@@ -18,26 +18,30 @@
|
|||||||
package org.keycloak.adapters.authorization;
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.OIDCHttpFacade;
|
import org.keycloak.adapters.OIDCHttpFacade;
|
||||||
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier;
|
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier;
|
||||||
import org.keycloak.adapters.spi.HttpFacade;
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
import org.keycloak.authorization.client.AuthorizationDeniedException;
|
import org.keycloak.authorization.client.AuthorizationDeniedException;
|
||||||
import org.keycloak.authorization.client.AuthzClient;
|
import org.keycloak.authorization.client.AuthzClient;
|
||||||
import org.keycloak.authorization.client.representation.AuthorizationRequest;
|
import org.keycloak.authorization.client.resource.PermissionResource;
|
||||||
import org.keycloak.authorization.client.representation.AuthorizationResponse;
|
import org.keycloak.authorization.client.resource.ProtectionResource;
|
||||||
import org.keycloak.authorization.client.representation.EntitlementRequest;
|
import org.keycloak.common.util.Base64;
|
||||||
import org.keycloak.authorization.client.representation.EntitlementResponse;
|
|
||||||
import org.keycloak.authorization.client.representation.PermissionRequest;
|
|
||||||
import org.keycloak.authorization.client.representation.PermissionResponse;
|
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||||
|
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
|
||||||
|
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
|
||||||
import org.keycloak.representations.idm.authorization.Permission;
|
import org.keycloak.representations.idm.authorization.Permission;
|
||||||
|
import org.keycloak.representations.idm.authorization.PermissionRequest;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
@@ -74,7 +78,14 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||||||
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
|
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
|
||||||
|
|
||||||
if (newAuthorization != null) {
|
if (newAuthorization != null) {
|
||||||
authorization.getPermissions().addAll(newAuthorization.getPermissions());
|
List<Permission> grantedPermissions = authorization.getPermissions();
|
||||||
|
List<Permission> newPermissions = newAuthorization.getPermissions();
|
||||||
|
|
||||||
|
for (Permission newPermission : newPermissions) {
|
||||||
|
if (!grantedPermissions.contains(newPermission)) {
|
||||||
|
grantedPermissions.add(newPermission);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
original.setAuthorization(authorization);
|
original.setAuthorization(authorization);
|
||||||
@@ -83,13 +94,40 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
|
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
|
||||||
handleAccessDenied(facade);
|
if (isBearerAuthorization(httpFacade)) {
|
||||||
|
HttpFacade.Response response = httpFacade.getResponse();
|
||||||
|
AuthzClient authzClient = getAuthzClient();
|
||||||
|
String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient, httpFacade);
|
||||||
|
|
||||||
|
if (ticket != null) {
|
||||||
|
response.setStatus(401);
|
||||||
|
response.setHeader("WWW-Authenticate", new StringBuilder("UMA realm=\"").append(authzClient.getConfiguration().getRealm()).append("\"").append(",as_uri=\"")
|
||||||
|
.append(authzClient.getServerConfiguration().getIssuer()).append("\"").append(",ticket=\"").append(ticket).append("\"").toString());
|
||||||
|
} else {
|
||||||
|
response.setStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debug("Sending challenge");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAccessDenied(httpFacade);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void handleAccessDenied(OIDCHttpFacade facade) {
|
protected void handleAccessDenied(OIDCHttpFacade facade) {
|
||||||
|
KeycloakSecurityContext securityContext = facade.getSecurityContext();
|
||||||
|
|
||||||
|
if (securityContext == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo();
|
String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo();
|
||||||
HttpFacade.Response response = facade.getResponse();
|
HttpFacade.Response response = facade.getResponse();
|
||||||
|
|
||||||
@@ -102,51 +140,88 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
|
private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
|
||||||
try {
|
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
||||||
String accessToken = httpFacade.getSecurityContext().getTokenString();
|
|
||||||
AuthzClient authzClient = getAuthzClient();
|
|
||||||
KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
|
|
||||||
|
|
||||||
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
|
||||||
LOGGER.debug("Obtaining authorization for authenticated user.");
|
|
||||||
PermissionRequest permissionRequest = new PermissionRequest();
|
|
||||||
|
|
||||||
permissionRequest.setResourceSetId(pathConfig.getId());
|
|
||||||
permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
|
|
||||||
|
|
||||||
PermissionResponse permissionResponse = authzClient.protection().permission().forResource(permissionRequest);
|
|
||||||
AuthorizationRequest authzRequest = new AuthorizationRequest(permissionResponse.getTicket());
|
|
||||||
AuthorizationResponse authzResponse = authzClient.authorization(accessToken).authorize(authzRequest);
|
|
||||||
|
|
||||||
if (authzResponse != null) {
|
|
||||||
return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
LOGGER.debug("Obtaining entitlements for authenticated user.");
|
|
||||||
AccessToken token = httpFacade.getSecurityContext().getToken();
|
|
||||||
|
|
||||||
if (token.getAuthorization() == null) {
|
|
||||||
EntitlementResponse authzResponse = authzClient.entitlement(accessToken).getAll(authzClient.getConfiguration().getResource());
|
|
||||||
return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
|
|
||||||
} else {
|
|
||||||
EntitlementRequest request = new EntitlementRequest();
|
|
||||||
PermissionRequest permissionRequest = new PermissionRequest();
|
|
||||||
permissionRequest.setResourceSetId(pathConfig.getId());
|
|
||||||
permissionRequest.setResourceSetName(pathConfig.getName());
|
|
||||||
permissionRequest.setScopes(new HashSet<>(pathConfig.getScopes()));
|
|
||||||
LOGGER.debugf("Sending entitlements request: resource_set_id [%s], resource_set_name [%s], scopes [%s].", permissionRequest.getResourceSetId(), permissionRequest.getResourceSetName(), permissionRequest.getScopes());
|
|
||||||
request.addPermission(permissionRequest);
|
|
||||||
EntitlementResponse authzResponse = authzClient.entitlement(accessToken).get(authzClient.getConfiguration().getResource(), request);
|
|
||||||
return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (AuthorizationDeniedException e) {
|
|
||||||
LOGGER.debug("Authorization denied", e);
|
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
|
||||||
|
String accessTokenString = securityContext.getTokenString();
|
||||||
|
KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
|
||||||
|
AccessToken accessToken = securityContext.getToken();
|
||||||
|
AuthorizationRequest authzRequest = new AuthorizationRequest();
|
||||||
|
|
||||||
|
if (isBearerAuthorization(httpFacade) || accessToken.getAuthorization() != null) {
|
||||||
|
authzRequest.addPermission(pathConfig.getId(), methodConfig.getScopes());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<String>> claims = resolveClaims(pathConfig, httpFacade);
|
||||||
|
|
||||||
|
if (!claims.isEmpty()) {
|
||||||
|
authzRequest.setClaimTokenFormat("urn:ietf:params:oauth:token-type:jwt");
|
||||||
|
authzRequest.setClaimToken(Base64.encodeBytes(JsonSerialization.writeValueAsBytes(claims)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessToken.getAuthorization() != null) {
|
||||||
|
authzRequest.setRpt(accessTokenString);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.debug("Obtaining authorization for authenticated user.");
|
||||||
|
AuthorizationResponse authzResponse;
|
||||||
|
|
||||||
|
if (isBearerAuthorization(httpFacade)) {
|
||||||
|
authzRequest.setSubjectToken(accessTokenString);
|
||||||
|
authzResponse = getAuthzClient().authorization().authorize(authzRequest);
|
||||||
|
} else {
|
||||||
|
authzResponse = getAuthzClient().authorization(accessTokenString).authorize(authzRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authzResponse != null) {
|
||||||
|
return AdapterRSATokenVerifier.verifyToken(authzResponse.getToken(), deployment);
|
||||||
|
}
|
||||||
|
} catch (AuthorizationDeniedException ignore) {
|
||||||
|
LOGGER.debug("Authorization denied", ignore);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Unexpected error during authorization request.", e);
|
throw new RuntimeException("Unexpected error during authorization request.", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPermissionTicket(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AuthzClient authzClient, OIDCHttpFacade httpFacade) {
|
||||||
|
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
||||||
|
ProtectionResource protection = authzClient.protection();
|
||||||
|
PermissionResource permission = protection.permission();
|
||||||
|
PermissionRequest permissionRequest = new PermissionRequest();
|
||||||
|
|
||||||
|
permissionRequest.setResourceId(pathConfig.getId());
|
||||||
|
permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
|
||||||
|
|
||||||
|
Map<String, List<String>> claims = resolveClaims(pathConfig, httpFacade);
|
||||||
|
|
||||||
|
if (!claims.isEmpty()) {
|
||||||
|
permissionRequest.setClaims(claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
return permission.create(permissionRequest).getTicket();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBearerAuthorization(OIDCHttpFacade httpFacade) {
|
||||||
|
List<String> authHeaders = httpFacade.getRequest().getHeaders("Authorization");
|
||||||
|
if (authHeaders == null || authHeaders.size() == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String authHeader : authHeaders) {
|
||||||
|
String[] split = authHeader.trim().split("\\s+");
|
||||||
|
if (split == null || split.length != 2) continue;
|
||||||
|
if (!split[0].equalsIgnoreCase("Bearer")) continue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+12
-9
@@ -24,6 +24,8 @@ import java.util.concurrent.locks.LockSupport;
|
|||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* A simple LRU cache implementation supporting expiration and maximum number of entries.
|
||||||
|
*
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
public class PathCache {
|
public class PathCache {
|
||||||
@@ -38,15 +40,7 @@ public class PathCache {
|
|||||||
private final AtomicBoolean writing = new AtomicBoolean(false);
|
private final AtomicBoolean writing = new AtomicBoolean(false);
|
||||||
|
|
||||||
private final long maxAge;
|
private final long maxAge;
|
||||||
|
private final boolean enabled;
|
||||||
/**
|
|
||||||
* Creates a new instance.
|
|
||||||
*
|
|
||||||
* @param maxEntries the maximum number of entries to keep in the cache
|
|
||||||
*/
|
|
||||||
public PathCache(int maxEntries) {
|
|
||||||
this(maxEntries, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance.
|
* Creates a new instance.
|
||||||
@@ -62,9 +56,14 @@ public class PathCache {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.maxAge = maxAge;
|
this.maxAge = maxAge;
|
||||||
|
this.enabled = maxAge > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void put(String uri, PathConfig newValue) {
|
public void put(String uri, PathConfig newValue) {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (parkForWriteAndCheckInterrupt()) {
|
if (parkForWriteAndCheckInterrupt()) {
|
||||||
return;
|
return;
|
||||||
@@ -80,6 +79,10 @@ public class PathCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean containsKey(String uri) {
|
||||||
|
return cache.containsKey(uri);
|
||||||
|
}
|
||||||
|
|
||||||
public PathConfig get(String uri) {
|
public PathConfig get(String uri) {
|
||||||
if (parkForReadAndCheckInterrupt()) {
|
if (parkForReadAndCheckInterrupt()) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+180
-89
@@ -17,6 +17,17 @@
|
|||||||
*/
|
*/
|
||||||
package org.keycloak.adapters.authorization;
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.ServiceLoader;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.AuthorizationContext;
|
import org.keycloak.AuthorizationContext;
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
@@ -25,22 +36,14 @@ import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
|
|||||||
import org.keycloak.authorization.client.AuthzClient;
|
import org.keycloak.authorization.client.AuthzClient;
|
||||||
import org.keycloak.authorization.client.ClientAuthenticator;
|
import org.keycloak.authorization.client.ClientAuthenticator;
|
||||||
import org.keycloak.authorization.client.Configuration;
|
import org.keycloak.authorization.client.Configuration;
|
||||||
import org.keycloak.authorization.client.representation.RegistrationResponse;
|
|
||||||
import org.keycloak.authorization.client.representation.ResourceRepresentation;
|
|
||||||
import org.keycloak.authorization.client.representation.ScopeRepresentation;
|
|
||||||
import org.keycloak.authorization.client.resource.ProtectedResource;
|
import org.keycloak.authorization.client.resource.ProtectedResource;
|
||||||
|
import org.keycloak.common.util.PathMatcher;
|
||||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||||
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathCacheConfig;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||||
import org.keycloak.representations.idm.authorization.Permission;
|
import org.keycloak.representations.idm.authorization.Permission;
|
||||||
|
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
@@ -52,20 +55,27 @@ public class PolicyEnforcer {
|
|||||||
private final KeycloakDeployment deployment;
|
private final KeycloakDeployment deployment;
|
||||||
private final AuthzClient authzClient;
|
private final AuthzClient authzClient;
|
||||||
private final PolicyEnforcerConfig enforcerConfig;
|
private final PolicyEnforcerConfig enforcerConfig;
|
||||||
|
private final PathConfigMatcher pathMatcher;
|
||||||
private final Map<String, PathConfig> paths;
|
private final Map<String, PathConfig> paths;
|
||||||
private final PathMatcher pathMatcher;
|
private final Map<String, ClaimInformationPointProviderFactory> claimInformationPointProviderFactories = new HashMap<>();
|
||||||
|
|
||||||
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
|
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
|
||||||
this.deployment = deployment;
|
this.deployment = deployment;
|
||||||
this.enforcerConfig = adapterConfig.getPolicyEnforcerConfig();
|
this.enforcerConfig = adapterConfig.getPolicyEnforcerConfig();
|
||||||
this.authzClient = AuthzClient.create(new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), deployment.getClient()), new ClientAuthenticator() {
|
Configuration configuration = new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), deployment.getClient());
|
||||||
|
this.authzClient = AuthzClient.create(configuration, new ClientAuthenticator() {
|
||||||
@Override
|
@Override
|
||||||
public void configureClientCredentials(HashMap<String, String> requestParams, HashMap<String, String> requestHeaders) {
|
public void configureClientCredentials(Map<String, List<String>> requestParams, Map<String, String> requestHeaders) {
|
||||||
ClientCredentialsProviderUtils.setClientCredentials(PolicyEnforcer.this.deployment, requestHeaders, requestParams);
|
Map<String, String> formparams = new HashMap<>();
|
||||||
|
ClientCredentialsProviderUtils.setClientCredentials(PolicyEnforcer.this.deployment, requestHeaders, formparams);
|
||||||
|
for (Entry<String, String> param : formparams.entrySet()) {
|
||||||
|
requestParams.put(param.getKey(), Arrays.asList(param.getValue()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.pathMatcher = new PathMatcher(this.authzClient);
|
|
||||||
this.paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
|
paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
|
||||||
|
pathMatcher = new PathConfigMatcher(paths, enforcerConfig, authzClient);
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debug("Initialization complete. Path configurations:");
|
LOGGER.debug("Initialization complete. Path configurations:");
|
||||||
@@ -73,20 +83,17 @@ public class PolicyEnforcer {
|
|||||||
LOGGER.debug(pathConfig);
|
LOGGER.debug(pathConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, ClaimInformationPointProviderFactory.class.getClassLoader()));
|
||||||
|
loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, Thread.currentThread().getContextClassLoader()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthorizationContext enforce(OIDCHttpFacade facade) {
|
public AuthorizationContext enforce(OIDCHttpFacade facade) {
|
||||||
if (LOGGER.isDebugEnabled()) {
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debugv("Policy enforcement is enable. Enforcing policy decisions for path [{0}].", facade.getRequest().getURI());
|
LOGGER.debugv("Policy enforcement is enabled. Enforcing policy decisions for path [{0}].", facade.getRequest().getURI());
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthorizationContext context;
|
AuthorizationContext context = new KeycloakAdapterPolicyEnforcer(this).authorize(facade);
|
||||||
|
|
||||||
if (deployment.isBearerOnly()) {
|
|
||||||
context = new BearerTokenPolicyEnforcer(this).authorize(facade);
|
|
||||||
} else {
|
|
||||||
context = new KeycloakAdapterPolicyEnforcer(this).authorize(facade);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debugv("Policy enforcement result for path [{0}] is : {1}", facade.getRequest().getURI(), context.isGranted() ? "GRANTED" : "DENIED");
|
LOGGER.debugv("Policy enforcement result for path [{0}] is : {1}", facade.getRequest().getURI(), context.isGranted() ? "GRANTED" : "DENIED");
|
||||||
@@ -99,11 +106,11 @@ public class PolicyEnforcer {
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
PolicyEnforcerConfig getEnforcerConfig() {
|
public PolicyEnforcerConfig getEnforcerConfig() {
|
||||||
return enforcerConfig;
|
return enforcerConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthzClient getClient() {
|
public AuthzClient getClient() {
|
||||||
return authzClient;
|
return authzClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,14 +118,30 @@ public class PolicyEnforcer {
|
|||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
void addPath(PathConfig pathConfig) {
|
public PathConfigMatcher getPathMatcher() {
|
||||||
paths.put(pathConfig.getPath(), pathConfig);
|
return pathMatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
KeycloakDeployment getDeployment() {
|
public KeycloakDeployment getDeployment() {
|
||||||
return deployment;
|
return deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, ClaimInformationPointProviderFactory> getClaimInformationPointProviderFactories() {
|
||||||
|
return claimInformationPointProviderFactories;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadClaimInformationPointProviders(ServiceLoader<ClaimInformationPointProviderFactory> loader) {
|
||||||
|
Iterator<ClaimInformationPointProviderFactory> iterator = loader.iterator();
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
ClaimInformationPointProviderFactory factory = iterator.next();
|
||||||
|
|
||||||
|
factory.init(this);
|
||||||
|
|
||||||
|
claimInformationPointProviderFactories.put(factory.getName(), factory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
private Map<String, PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
||||||
boolean loadPathsFromServer = true;
|
boolean loadPathsFromServer = true;
|
||||||
|
|
||||||
@@ -131,7 +154,11 @@ public class PolicyEnforcer {
|
|||||||
|
|
||||||
if (loadPathsFromServer) {
|
if (loadPathsFromServer) {
|
||||||
LOGGER.info("No path provided in configuration.");
|
LOGGER.info("No path provided in configuration.");
|
||||||
return configureAllPathsForResourceServer(protectedResource);
|
Map<String, PathConfig> paths = configureAllPathsForResourceServer(protectedResource);
|
||||||
|
|
||||||
|
paths.putAll(configureDefinedPaths(protectedResource, enforcerConfig));
|
||||||
|
|
||||||
|
return paths;
|
||||||
} else {
|
} else {
|
||||||
LOGGER.info("Paths provided in configuration.");
|
LOGGER.info("Paths provided in configuration.");
|
||||||
return configureDefinedPaths(protectedResource, enforcerConfig);
|
return configureDefinedPaths(protectedResource, enforcerConfig);
|
||||||
@@ -139,56 +166,41 @@ public class PolicyEnforcer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
private Map<String, PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
||||||
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
|
Map<String, PathConfig> paths = Collections.synchronizedMap(new LinkedHashMap<String, PathConfig>());
|
||||||
|
|
||||||
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
|
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
|
||||||
Set<String> search;
|
ResourceRepresentation resource;
|
||||||
String resourceName = pathConfig.getName();
|
String resourceName = pathConfig.getName();
|
||||||
String path = pathConfig.getPath();
|
String path = pathConfig.getPath();
|
||||||
|
|
||||||
if (resourceName != null) {
|
if (resourceName != null) {
|
||||||
LOGGER.debugf("Trying to find resource with name [%s] for path [%s].", resourceName, path);
|
LOGGER.debugf("Trying to find resource with name [%s] for path [%s].", resourceName, path);
|
||||||
search = protectedResource.findByFilter("name=" + resourceName);
|
resource = protectedResource.findByName(resourceName);
|
||||||
} else {
|
} else {
|
||||||
LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path);
|
LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path);
|
||||||
search = protectedResource.findByFilter("uri=" + path);
|
List<ResourceRepresentation> resources = protectedResource.findByUri(path);
|
||||||
|
|
||||||
|
if (resources.isEmpty()) {
|
||||||
|
resources = protectedResource.findByMatchingUri(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resources.size() == 1) {
|
||||||
|
resource = resources.get(0);
|
||||||
|
} else if (resources.size() > 1) {
|
||||||
|
throw new RuntimeException("Multiple resources found with the same uri");
|
||||||
|
} else {
|
||||||
|
resource = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search.isEmpty()) {
|
if (resource != null) {
|
||||||
if (enforcerConfig.isCreateResources()) {
|
pathConfig.setId(resource.getId());
|
||||||
LOGGER.debugf("Creating resource on server for path [%s].", pathConfig);
|
|
||||||
ResourceRepresentation resource = new ResourceRepresentation();
|
|
||||||
|
|
||||||
resource.setName(resourceName);
|
|
||||||
resource.setType(pathConfig.getType());
|
|
||||||
resource.setUri(path);
|
|
||||||
|
|
||||||
HashSet<ScopeRepresentation> scopes = new HashSet<>();
|
|
||||||
|
|
||||||
for (String scopeName : pathConfig.getScopes()) {
|
|
||||||
ScopeRepresentation scope = new ScopeRepresentation();
|
|
||||||
|
|
||||||
scope.setName(scopeName);
|
|
||||||
|
|
||||||
scopes.add(scope);
|
|
||||||
}
|
|
||||||
|
|
||||||
resource.setScopes(scopes);
|
|
||||||
|
|
||||||
RegistrationResponse registrationResponse = protectedResource.create(resource);
|
|
||||||
|
|
||||||
pathConfig.setId(registrationResponse.getId());
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException("Could not find matching resource on server with uri [" + path + "] or name [" + resourceName + "]. Make sure you have created a resource on the server that matches with the path configuration.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pathConfig.setId(search.iterator().next());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PathConfig existingPath = null;
|
PathConfig existingPath = null;
|
||||||
|
|
||||||
for (PathConfig current : paths.values()) {
|
for (PathConfig current : paths.values()) {
|
||||||
if (current.getId().equals(pathConfig.getId()) && current.getPath().equals(pathConfig.getPath())) {
|
if (current.getPath().equals(pathConfig.getPath())) {
|
||||||
existingPath = current;
|
existingPath = current;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -209,46 +221,125 @@ public class PolicyEnforcer {
|
|||||||
LOGGER.info("Querying the server for all resources associated with this application.");
|
LOGGER.info("Querying the server for all resources associated with this application.");
|
||||||
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
|
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
|
||||||
|
|
||||||
for (String id : protectedResource.findAll()) {
|
if (!enforcerConfig.getLazyLoadPaths()) {
|
||||||
RegistrationResponse response = protectedResource.findById(id);
|
for (String id : protectedResource.findAll()) {
|
||||||
ResourceRepresentation resourceDescription = response.getResourceDescription();
|
ResourceRepresentation resourceDescription = protectedResource.findById(id);
|
||||||
|
|
||||||
if (resourceDescription.getUri() != null) {
|
if (resourceDescription.getUri() != null) {
|
||||||
PathConfig pathConfig = createPathConfig(resourceDescription);
|
PathConfig pathConfig = PathConfig.createPathConfig(resourceDescription);
|
||||||
paths.put(pathConfig.getPath(), pathConfig);
|
paths.put(pathConfig.getPath(), pathConfig);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
static PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
|
public class PathConfigMatcher extends PathMatcher<PathConfig> {
|
||||||
PathConfig pathConfig = new PathConfig();
|
|
||||||
|
|
||||||
pathConfig.setId(resourceDescription.getId());
|
private final Map<String, PathConfig> paths;
|
||||||
pathConfig.setName(resourceDescription.getName());
|
private final PathCache pathCache;
|
||||||
|
private final AuthzClient authzClient;
|
||||||
|
private final PolicyEnforcerConfig enforcerConfig;
|
||||||
|
|
||||||
String uri = resourceDescription.getUri();
|
public PathConfigMatcher(Map<String, PathConfig> paths, PolicyEnforcerConfig enforcerConfig, AuthzClient authzClient) {
|
||||||
|
this.paths = paths;
|
||||||
|
this.enforcerConfig = enforcerConfig;
|
||||||
|
PathCacheConfig cacheConfig = enforcerConfig.getPathCacheConfig();
|
||||||
|
|
||||||
if (uri == null || "".equals(uri.trim())) {
|
if (cacheConfig == null) {
|
||||||
throw new RuntimeException("Failed to configure paths. Resource [" + resourceDescription.getName() + "] has an invalid or empty URI [" + uri + "].");
|
cacheConfig = new PathCacheConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
pathCache = new PathCache(cacheConfig.getMaxEntries(), cacheConfig.getLifespan());
|
||||||
|
this.authzClient = authzClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
pathConfig.setPath(uri);
|
@Override
|
||||||
|
public PathConfig matches(String targetUri) {
|
||||||
|
PathConfig pathConfig = pathCache.get(targetUri);
|
||||||
|
|
||||||
List<String> scopeNames = new ArrayList<>();
|
if (pathCache.containsKey(targetUri) || pathConfig != null) {
|
||||||
|
return pathConfig;
|
||||||
|
}
|
||||||
|
|
||||||
for (ScopeRepresentation scope : resourceDescription.getScopes()) {
|
pathConfig = super.matches(targetUri);
|
||||||
scopeNames.add(scope.getName());
|
|
||||||
|
if (enforcerConfig.getLazyLoadPaths() || enforcerConfig.getPathCacheConfig() != null) {
|
||||||
|
if ((pathConfig == null || (pathConfig.getPath().contains("*")))) {
|
||||||
|
try {
|
||||||
|
List<ResourceRepresentation> matchingResources = authzClient.protection().resource().findByMatchingUri(targetUri);
|
||||||
|
|
||||||
|
if (!matchingResources.isEmpty()) {
|
||||||
|
Map<String, Map<String, Object>> cipConfig = null;
|
||||||
|
|
||||||
|
if (pathConfig != null) {
|
||||||
|
cipConfig = pathConfig.getClaimInformationPointConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
pathConfig = PathConfig.createPathConfig(matchingResources.get(0));
|
||||||
|
|
||||||
|
if (cipConfig != null) {
|
||||||
|
pathConfig.setClaimInformationPointConfig(cipConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception cause) {
|
||||||
|
LOGGER.errorf(cause, "Could not lazy load resource with path [" + targetUri + "] from server");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pathCache.put(targetUri, pathConfig);
|
||||||
|
|
||||||
|
return pathConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
pathConfig.setScopes(scopeNames);
|
@Override
|
||||||
pathConfig.setType(resourceDescription.getType());
|
protected String getPath(PathConfig entry) {
|
||||||
|
return entry.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
return pathConfig;
|
@Override
|
||||||
}
|
protected Collection<PathConfig> getPaths() {
|
||||||
|
return paths.values();
|
||||||
|
}
|
||||||
|
|
||||||
public PathMatcher getPathMatcher() {
|
@Override
|
||||||
return pathMatcher;
|
protected PathConfig resolvePathConfig(PathConfig originalConfig, String path) {
|
||||||
}
|
if (originalConfig.hasPattern()) {
|
||||||
|
ProtectedResource resource = authzClient.protection().resource();
|
||||||
|
|
||||||
|
// search by an exact match
|
||||||
|
List<ResourceRepresentation> search = resource.findByUri(path);
|
||||||
|
|
||||||
|
// if exact match not found, try to obtain from current path the parent path.
|
||||||
|
// if path is /resource/1/test and pattern from pathConfig is /resource/{id}/*, parent path is /resource/1
|
||||||
|
// this logic allows to match sub resources of a resource instance (/resource/1) to the parent resource,
|
||||||
|
// so any permission granted to parent also applies to sub resources
|
||||||
|
if (search.isEmpty()) {
|
||||||
|
search = resource.findByUri(buildUriFromTemplate(originalConfig.getPath(), path, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!search.isEmpty()) {
|
||||||
|
ResourceRepresentation targetResource = search.get(0);
|
||||||
|
PathConfig config = PathConfig.createPathConfig(targetResource);
|
||||||
|
|
||||||
|
config.setScopes(originalConfig.getScopes());
|
||||||
|
config.setMethods(originalConfig.getMethods());
|
||||||
|
config.setParentConfig(originalConfig);
|
||||||
|
config.setEnforcementMode(originalConfig.getEnforcementMode());
|
||||||
|
config.setClaimInformationPointConfig(originalConfig.getClaimInformationPointConfig());
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeFromCache(String pathConfig) {
|
||||||
|
pathCache.remove(pathConfig);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.authorization.cip;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.authorization.ClaimInformationPointProvider;
|
||||||
|
import org.keycloak.adapters.authorization.util.PlaceHolders;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class ClaimsInformationPointProvider implements ClaimInformationPointProvider {
|
||||||
|
|
||||||
|
private final Map<String, Object> config;
|
||||||
|
|
||||||
|
public ClaimsInformationPointProvider(Map<String, Object> config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, List<String>> resolve(HttpFacade httpFacade) {
|
||||||
|
Map<String, List<String>> claims = new HashMap<>();
|
||||||
|
|
||||||
|
for (Entry<String, Object> configEntry : config.entrySet()) {
|
||||||
|
String claimName = configEntry.getKey();
|
||||||
|
Object claimValue = configEntry.getValue();
|
||||||
|
List<String> values = new ArrayList<>();
|
||||||
|
|
||||||
|
if (claimValue instanceof String) {
|
||||||
|
values = getValues(claimValue.toString(), httpFacade);
|
||||||
|
} else if (claimValue instanceof Collection) {
|
||||||
|
Iterator iterator = Collection.class.cast(claimValue).iterator();
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
List<String> resolvedValues = getValues(iterator.next().toString(), httpFacade);
|
||||||
|
|
||||||
|
if (!resolvedValues.isEmpty()) {
|
||||||
|
values.addAll(resolvedValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.isEmpty()) {
|
||||||
|
claims.put(claimName, values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getValues(String value, HttpFacade httpFacade) {
|
||||||
|
return PlaceHolders.resolve(value, httpFacade);
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.authorization.cip;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory;
|
||||||
|
import org.keycloak.adapters.authorization.PolicyEnforcer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class ClaimsInformationPointProviderFactory implements ClaimInformationPointProviderFactory<ClaimsInformationPointProvider> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "claims";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(PolicyEnforcer policyEnforcer) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClaimsInformationPointProvider create(Map<String, Object> config) {
|
||||||
|
return new ClaimsInformationPointProvider(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
+210
@@ -0,0 +1,210 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.authorization.cip;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import org.apache.http.HttpEntity;
|
||||||
|
import org.apache.http.HttpResponse;
|
||||||
|
import org.apache.http.StatusLine;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.apache.http.client.methods.RequestBuilder;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.keycloak.adapters.authorization.ClaimInformationPointProvider;
|
||||||
|
import org.keycloak.adapters.authorization.PolicyEnforcer;
|
||||||
|
import org.keycloak.adapters.authorization.util.JsonUtils;
|
||||||
|
import org.keycloak.adapters.authorization.util.PlaceHolders;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.authorization.client.util.HttpResponseException;
|
||||||
|
import org.keycloak.common.util.StreamUtil;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class HttpClaimInformationPointProvider implements ClaimInformationPointProvider {
|
||||||
|
|
||||||
|
private final Map<String, Object> config;
|
||||||
|
private final HttpClient httpClient;
|
||||||
|
|
||||||
|
public HttpClaimInformationPointProvider(Map<String, Object> config, PolicyEnforcer policyEnforcer) {
|
||||||
|
this.config = config;
|
||||||
|
this.httpClient = policyEnforcer.getDeployment().getClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, List<String>> resolve(HttpFacade httpFacade) {
|
||||||
|
try {
|
||||||
|
InputStream responseStream = executeRequest(httpFacade);
|
||||||
|
|
||||||
|
try (InputStream inputStream = new BufferedInputStream(responseStream)) {
|
||||||
|
JsonNode jsonNode = JsonSerialization.mapper.readTree(inputStream);
|
||||||
|
Map<String, List<String>> claims = new HashMap<>();
|
||||||
|
Map<String, Object> claimsDef = (Map<String, Object>) config.get("claims");
|
||||||
|
|
||||||
|
if (claimsDef == null) {
|
||||||
|
Iterator<String> nodeNames = jsonNode.fieldNames();
|
||||||
|
|
||||||
|
while (nodeNames.hasNext()) {
|
||||||
|
String nodeName = nodeNames.next();
|
||||||
|
claims.put(nodeName, JsonUtils.getValues(jsonNode.get(nodeName)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (Entry<String, Object> claimDef : claimsDef.entrySet()) {
|
||||||
|
List<String> jsonPaths = new ArrayList<>();
|
||||||
|
|
||||||
|
if (claimDef.getValue() instanceof Collection) {
|
||||||
|
jsonPaths.addAll(Collection.class.cast(claimDef.getValue()));
|
||||||
|
} else {
|
||||||
|
jsonPaths.add(claimDef.getValue().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> claimValues = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String path : jsonPaths) {
|
||||||
|
claimValues.addAll(JsonUtils.getValues(jsonNode, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
claims.put(claimDef.getKey(), claimValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Could not obtain claims from http claim information point [" + config.get("url") + "] response", cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputStream executeRequest(HttpFacade httpFacade) {
|
||||||
|
String method = config.get("method").toString();
|
||||||
|
|
||||||
|
if (method == null) {
|
||||||
|
method = "GET";
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder builder = null;
|
||||||
|
|
||||||
|
if ("GET".equalsIgnoreCase(method)) {
|
||||||
|
builder = RequestBuilder.get();
|
||||||
|
} else {
|
||||||
|
builder = RequestBuilder.post();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setUri(config.get("url").toString());
|
||||||
|
|
||||||
|
byte[] bytes = new byte[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
setParameters(builder, httpFacade);
|
||||||
|
|
||||||
|
if (config.containsKey("headers")) {
|
||||||
|
setHeaders(builder, httpFacade);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse response = httpClient.execute(builder.build());
|
||||||
|
HttpEntity entity = response.getEntity();
|
||||||
|
|
||||||
|
if (entity != null) {
|
||||||
|
bytes = EntityUtils.toByteArray(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusLine statusLine = response.getStatusLine();
|
||||||
|
int statusCode = statusLine.getStatusCode();
|
||||||
|
|
||||||
|
if (statusCode < 200 || statusCode >= 300) {
|
||||||
|
throw new HttpResponseException("Unexpected response from server: " + statusCode + " / " + statusLine.getReasonPhrase(), statusCode, statusLine.getReasonPhrase(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ByteArrayInputStream(bytes);
|
||||||
|
} catch (Exception cause) {
|
||||||
|
try {
|
||||||
|
throw new RuntimeException("Error executing http method [" + builder + "]. Response : " + StreamUtil.readString(new ByteArrayInputStream(bytes), Charset.forName("UTF-8")), cause);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Error executing http method [" + builder + "]", cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setHeaders(RequestBuilder builder, HttpFacade httpFacade) {
|
||||||
|
Object headersDef = config.get("headers");
|
||||||
|
|
||||||
|
if (headersDef != null) {
|
||||||
|
Map<String, Object> headers = Map.class.cast(headersDef);
|
||||||
|
|
||||||
|
for (Entry<String, Object> header : headers.entrySet()) {
|
||||||
|
Object value = header.getValue();
|
||||||
|
List<String> headerValues = new ArrayList<>();
|
||||||
|
|
||||||
|
if (value instanceof Collection) {
|
||||||
|
Collection values = Collection.class.cast(value);
|
||||||
|
Iterator iterator = values.iterator();
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
headerValues.addAll(PlaceHolders.resolve(iterator.next().toString(), httpFacade));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headerValues.addAll(PlaceHolders.resolve(value.toString(), httpFacade));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String headerValue : headerValues) {
|
||||||
|
builder.addHeader(header.getKey(), headerValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setParameters(RequestBuilder builder, HttpFacade httpFacade) {
|
||||||
|
Object config = this.config.get("parameters");
|
||||||
|
|
||||||
|
if (config != null) {
|
||||||
|
Map<String, Object> paramsDef = Map.class.cast(config);
|
||||||
|
|
||||||
|
for (Entry<String, Object> paramDef : paramsDef.entrySet()) {
|
||||||
|
Object value = paramDef.getValue();
|
||||||
|
List<String> paramValues = new ArrayList<>();
|
||||||
|
|
||||||
|
if (value instanceof Collection) {
|
||||||
|
Collection values = Collection.class.cast(value);
|
||||||
|
Iterator iterator = values.iterator();
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
paramValues.addAll(PlaceHolders.resolve(iterator.next().toString(), httpFacade));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
paramValues.addAll(PlaceHolders.resolve(value.toString(), httpFacade));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String paramValue : paramValues) {
|
||||||
|
builder.addParameter(paramDef.getKey(), paramValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.authorization.cip;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory;
|
||||||
|
import org.keycloak.adapters.authorization.PolicyEnforcer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class HttpClaimInformationPointProviderFactory implements ClaimInformationPointProviderFactory<HttpClaimInformationPointProvider> {
|
||||||
|
|
||||||
|
private PolicyEnforcer policyEnforcer;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "http";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(PolicyEnforcer policyEnforcer) {
|
||||||
|
this.policyEnforcer = policyEnforcer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClaimInformationPointProvider create(Map<String, Object> config) {
|
||||||
|
return new HttpClaimInformationPointProvider(config, policyEnforcer);
|
||||||
|
}
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.authorization.util;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class JsonUtils {
|
||||||
|
|
||||||
|
public static List<String> getValues(JsonNode jsonNode, String path) {
|
||||||
|
return getValues(jsonNode.at(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<String> getValues(JsonNode jsonNode) {
|
||||||
|
List<String> values = new ArrayList<>();
|
||||||
|
|
||||||
|
if (jsonNode.isArray()) {
|
||||||
|
Iterator<JsonNode> iterator = jsonNode.iterator();
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
String value = iterator.next().textValue();
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
values.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String value = jsonNode.textValue();
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
values.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.authorization.util;
|
||||||
|
|
||||||
|
import static org.keycloak.adapters.authorization.util.PlaceHolders.getParameter;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.OIDCHttpFacade;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class KeycloakSecurityContextPlaceHolderResolver implements PlaceHolderResolver {
|
||||||
|
|
||||||
|
public static final String NAME = "keycloak";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> resolve(String placeHolder, HttpFacade httpFacade) {
|
||||||
|
String source = placeHolder.substring(placeHolder.indexOf('.') + 1);
|
||||||
|
OIDCHttpFacade oidcHttpFacade = OIDCHttpFacade.class.cast(httpFacade);
|
||||||
|
KeycloakSecurityContext securityContext = oidcHttpFacade.getSecurityContext();
|
||||||
|
|
||||||
|
if (securityContext == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.endsWith("access_token")) {
|
||||||
|
return Arrays.asList(securityContext.getTokenString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.endsWith("id_token")) {
|
||||||
|
return Arrays.asList(securityContext.getIdTokenString());
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode jsonNode;
|
||||||
|
|
||||||
|
if (source.startsWith("access_token[")) {
|
||||||
|
jsonNode = JsonSerialization.mapper.valueToTree(securityContext.getToken());
|
||||||
|
} else if (source.startsWith("id_token[")) {
|
||||||
|
jsonNode = JsonSerialization.mapper.valueToTree(securityContext.getIdToken());
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Invalid placeholder [" + placeHolder + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonUtils.getValues(jsonNode, getParameter(source, "Invalid placeholder [" + placeHolder + "]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.authorization.util;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public interface PlaceHolderResolver {
|
||||||
|
|
||||||
|
List<String> resolve(String placeHolder, HttpFacade httpFacade);
|
||||||
|
|
||||||
|
}
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.authorization.util;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class PlaceHolders {
|
||||||
|
|
||||||
|
private static Map<String, PlaceHolderResolver> resolvers = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
resolvers.put(RequestPlaceHolderResolver.NAME, new RequestPlaceHolderResolver());
|
||||||
|
resolvers.put(KeycloakSecurityContextPlaceHolderResolver.NAME, new KeycloakSecurityContextPlaceHolderResolver());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{(.+?)\\}");
|
||||||
|
private static Pattern PLACEHOLDER_PARAM_PATTERN = Pattern.compile("\\[(.+?)\\]");
|
||||||
|
|
||||||
|
public static List<String> resolve(String value, HttpFacade httpFacade) {
|
||||||
|
Map<String, List<String>> placeHolders = parsePlaceHolders(value, httpFacade);
|
||||||
|
|
||||||
|
if (!placeHolders.isEmpty()) {
|
||||||
|
value = formatPlaceHolder(value);
|
||||||
|
|
||||||
|
for (Entry<String, List<String>> entry : placeHolders.entrySet()) {
|
||||||
|
List<String> values = entry.getValue();
|
||||||
|
|
||||||
|
if (values.isEmpty() || values.size() > 1) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.replaceAll(entry.getKey(), values.get(0)).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.asList(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getParameter(String source, String messageIfNotFound) {
|
||||||
|
Matcher matcher = PLACEHOLDER_PARAM_PATTERN.matcher(source);
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
return matcher.group(1).replaceAll("'", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageIfNotFound != null) {
|
||||||
|
throw new RuntimeException(messageIfNotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, List<String>> parsePlaceHolders(String value, HttpFacade httpFacade) {
|
||||||
|
Map<String, List<String>> placeHolders = new HashMap<>();
|
||||||
|
Matcher matcher = PLACEHOLDER_PATTERN.matcher(value);
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
String placeHolder = matcher.group(1);
|
||||||
|
int resolverNameIdx = placeHolder.indexOf('.');
|
||||||
|
|
||||||
|
if (resolverNameIdx == -1) {
|
||||||
|
throw new RuntimeException("Invalid placeholder [" + value + "]. Could not find resolver name.");
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaceHolderResolver resolver = resolvers.get(placeHolder.substring(0, resolverNameIdx));
|
||||||
|
|
||||||
|
if (resolver != null) {
|
||||||
|
List<String> resolved = resolver.resolve(placeHolder, httpFacade);
|
||||||
|
|
||||||
|
if (resolved != null) {
|
||||||
|
placeHolders.put(formatPlaceHolder(placeHolder), resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return placeHolders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatPlaceHolder(String placeHolder) {
|
||||||
|
return placeHolder.replaceAll("\\{", "").replace("}", "").replace("[", "").replace("]", "").replace("[", "").replace("]", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
+175
@@ -0,0 +1,175 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.authorization.util;
|
||||||
|
|
||||||
|
import static org.keycloak.adapters.authorization.util.PlaceHolders.getParameter;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade.Cookie;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade.Request;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class RequestPlaceHolderResolver implements PlaceHolderResolver {
|
||||||
|
|
||||||
|
static String NAME = "request";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> resolve(String placeHolder, HttpFacade httpFacade) {
|
||||||
|
String source = placeHolder.substring(placeHolder.indexOf('.') + 1);
|
||||||
|
Request request = httpFacade.getRequest();
|
||||||
|
|
||||||
|
if (source.startsWith("parameter")) {
|
||||||
|
String parameterName = getParameter(source, "Could not obtain parameter name from placeholder [" + source + "]");
|
||||||
|
String parameterValue = request.getQueryParamValue(parameterName);
|
||||||
|
|
||||||
|
if (parameterValue == null) {
|
||||||
|
parameterValue = request.getFirstParam(parameterName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterValue != null) {
|
||||||
|
return Arrays.asList(parameterValue);
|
||||||
|
}
|
||||||
|
} else if (source.startsWith("header")) {
|
||||||
|
String headerName = getParameter(source, "Could not obtain header name from placeholder [" + source + "]");
|
||||||
|
List<String> headerValue = request.getHeaders(headerName);
|
||||||
|
|
||||||
|
if (headerValue != null) {
|
||||||
|
return headerValue;
|
||||||
|
}
|
||||||
|
} else if (source.startsWith("cookie")) {
|
||||||
|
String cookieName = getParameter(source, "Could not obtain cookie name from placeholder [" + source + "]");
|
||||||
|
Cookie cookieValue = request.getCookie(cookieName);
|
||||||
|
|
||||||
|
if (cookieValue != null) {
|
||||||
|
return Arrays.asList(cookieValue.getValue());
|
||||||
|
}
|
||||||
|
} else if (source.startsWith("remoteAddr")) {
|
||||||
|
String value = request.getRemoteAddr();
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
return Arrays.asList(value);
|
||||||
|
}
|
||||||
|
} else if (source.startsWith("method")) {
|
||||||
|
String value = request.getMethod();
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
return Arrays.asList(value);
|
||||||
|
}
|
||||||
|
} else if (source.startsWith("uri")) {
|
||||||
|
String value = request.getURI();
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
return Arrays.asList(value);
|
||||||
|
}
|
||||||
|
} else if (source.startsWith("relativePath")) {
|
||||||
|
String value = request.getRelativePath();
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
return Arrays.asList(value);
|
||||||
|
}
|
||||||
|
} else if (source.startsWith("secure")) {
|
||||||
|
return Arrays.asList(String.valueOf(request.isSecure()));
|
||||||
|
} else if (source.startsWith("body")) {
|
||||||
|
String contentType = request.getHeader("Content-Type");
|
||||||
|
|
||||||
|
if (contentType == null) {
|
||||||
|
contentType = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream body = request.getInputStream(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (body == null || body.available() == 0) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Failed to check available bytes in request input stream", cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.markSupported()) {
|
||||||
|
body.mark(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> values = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (contentType) {
|
||||||
|
case "application/json":
|
||||||
|
try {
|
||||||
|
JsonNode jsonNode = JsonSerialization.mapper.readTree(new BufferedInputStream(body) {
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// we can't close the stream because it may be used later by the application
|
||||||
|
}
|
||||||
|
});
|
||||||
|
String path = getParameter(source, null);
|
||||||
|
|
||||||
|
if (path == null) {
|
||||||
|
values.addAll(JsonUtils.getValues(jsonNode));
|
||||||
|
} else {
|
||||||
|
values.addAll(JsonUtils.getValues(jsonNode, path));
|
||||||
|
}
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Could not extract claim from request JSON body", cause);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
StringBuilder value = new StringBuilder();
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(body));
|
||||||
|
|
||||||
|
try {
|
||||||
|
int ch;
|
||||||
|
|
||||||
|
while ((ch = reader.read()) != -1) {
|
||||||
|
value.append((char) ch);
|
||||||
|
}
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Could not extract claim from request body", cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
values.add(value.toString());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (body.markSupported()) {
|
||||||
|
try {
|
||||||
|
body.reset();
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Failed to reset request input stream", cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -16,4 +16,5 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider
|
org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider
|
||||||
org.keycloak.adapters.authentication.JWTClientCredentialsProvider
|
org.keycloak.adapters.authentication.JWTClientCredentialsProvider
|
||||||
|
org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#
|
||||||
|
# * Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
# * and other contributors as indicated by the @author tags.
|
||||||
|
# *
|
||||||
|
# * Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# * you may not use this file except in compliance with the License.
|
||||||
|
# * You may obtain a copy of the License at
|
||||||
|
# *
|
||||||
|
# * http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
# *
|
||||||
|
# * Unless required by applicable law or agreed to in writing, software
|
||||||
|
# * distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# * See the License for the specific language governing permissions and
|
||||||
|
# * limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
org.keycloak.adapters.authorization.cip.ClaimsInformationPointProviderFactory
|
||||||
|
org.keycloak.adapters.authorization.cip.HttpClaimInformationPointProviderFactory
|
||||||
+6
@@ -21,6 +21,7 @@ import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider;
|
import org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider;
|
||||||
import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
|
import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
|
||||||
|
import org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider;
|
||||||
import org.keycloak.adapters.rotation.HardcodedPublicKeyLocator;
|
import org.keycloak.adapters.rotation.HardcodedPublicKeyLocator;
|
||||||
import org.keycloak.adapters.rotation.JWKPublicKeyLocator;
|
import org.keycloak.adapters.rotation.JWKPublicKeyLocator;
|
||||||
import org.keycloak.common.enums.RelativeUrlsUsed;
|
import org.keycloak.common.enums.RelativeUrlsUsed;
|
||||||
@@ -94,4 +95,9 @@ public class KeycloakDeploymentBuilderTest {
|
|||||||
assertEquals(JWTClientCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
|
assertEquals(JWTClientCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void loadSecretJwtCredentials() throws Exception {
|
||||||
|
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak-secret-jwt.json"));
|
||||||
|
assertEquals(JWTClientSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"realm": "demo",
|
||||||
|
"resource": "customer-portal",
|
||||||
|
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||||
|
"auth-server-url": "https://localhost:8443/auth",
|
||||||
|
"ssl-required": "external",
|
||||||
|
"credentials": {
|
||||||
|
"secret-jwt": {
|
||||||
|
"secret": "234234-234234-234234"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<name>Keycloak AS7 / JBoss EAP 6 Integration</name>
|
<name>Keycloak AS7 / JBoss EAP 6 Integration</name>
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
export KC_AUTH_SERVER=http://localhost:8080/auth
|
|
||||||
export KC_REALM=master
|
|
||||||
export KC_CLIENT=cli
|
|
||||||
|
|
||||||
export KC_ACCESS_TOKEN=`java -DKEYCLOAK_AUTH_SERVER=$KC_AUTH_SERVER -DKEYCLOAK_REALM=$KC_REALM -DKEYCLOAK_CLIENT=$KC_CLIENT -jar target/keycloak-cli-sso-3.3.0.CR1-SNAPSHOT.jar login`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
java -DKEYCLOAK_AUTH_SERVER=$KC_AUTH_SERVER -DKEYCLOAK_REALM=$KC_REALM -DKEYCLOAK_CLIENT=$KC_CLIENT -jar target/keycloak-cli-sso-3.3.0.CR1-SNAPSHOT.jar logout
|
|
||||||
|
|
||||||
unset KC_ACCESS_TOKEN
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
|
||||||
* and other contributors as indicated by the @author tags.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.keycloak.adapters;
|
|
||||||
|
|
||||||
import org.keycloak.adapters.installed.KeycloakCliSso;
|
|
||||||
import org.keycloak.adapters.installed.KeycloakInstalled;
|
|
||||||
import org.keycloak.common.util.Time;
|
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
|
||||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
|
||||||
import org.keycloak.util.JsonSerialization;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
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 KeycloakCliSsoMain extends KeycloakCliSso {
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
new KeycloakCliSsoMain().mainCmd(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-fuse7-integration-pom</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>4.0.0.Final</version>
|
||||||
|
<relativePath>../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-camel-undertow</artifactId>
|
||||||
|
<name>Keycloak Fuse 7.0 Adapter - Camel + Undertow</name>
|
||||||
|
<packaging>bundle</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<keycloak.osgi.export>
|
||||||
|
org.keycloak.adapters.camel.undertow;version="${project.version}"
|
||||||
|
</keycloak.osgi.export>
|
||||||
|
<keycloak.osgi.import>
|
||||||
|
org.keycloak.*;version="${project.version}",
|
||||||
|
org.apache.camel.*,
|
||||||
|
org.apache.camel.component.undertow,
|
||||||
|
io.undertow.*,
|
||||||
|
*;resolution:=optional
|
||||||
|
</keycloak.osgi.import>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.logging</groupId>
|
||||||
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.osgi</groupId>
|
||||||
|
<artifactId>org.osgi.core</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.osgi</groupId>
|
||||||
|
<artifactId>org.osgi.enterprise</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-runtime</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-spi</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-api</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.undertow</groupId>
|
||||||
|
<artifactId>undertow-servlet</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-undertow-adapter</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel</groupId>
|
||||||
|
<artifactId>camel-undertow</artifactId>
|
||||||
|
<version>2.21.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel</groupId>
|
||||||
|
<artifactId>camel-core</artifactId>
|
||||||
|
<version>2.21.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<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-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>
|
||||||
|
<Export-Service>org.apache.camel.spi.ComponentResolver;component=undertow-keycloak</Export-Service>
|
||||||
|
</instructions>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.camel.undertow;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.apache.camel.CamelContext;
|
||||||
|
import org.apache.camel.Consumer;
|
||||||
|
import org.apache.camel.Processor;
|
||||||
|
import org.apache.camel.component.undertow.RestUndertowHttpBinding;
|
||||||
|
import org.apache.camel.component.undertow.UndertowComponent;
|
||||||
|
import org.apache.camel.component.undertow.UndertowEndpoint;
|
||||||
|
import org.apache.camel.spi.RestConfiguration;
|
||||||
|
import org.apache.camel.util.FileUtil;
|
||||||
|
import org.apache.camel.util.HostUtils;
|
||||||
|
import org.apache.camel.util.ObjectHelper;
|
||||||
|
import org.apache.camel.util.URISupport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class UndertowKeycloakComponent extends UndertowComponent {
|
||||||
|
|
||||||
|
public UndertowKeycloakComponent() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public UndertowKeycloakComponent(CamelContext context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected UndertowEndpoint createEndpointInstance(URI endpointUri, UndertowComponent component) throws URISyntaxException {
|
||||||
|
return new UndertowKeycloakEndpoint(endpointUri.toString(), component);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: uncomment line below after backport of https://issues.apache.org/jira/browse/CAMEL-12514 into fuse
|
||||||
|
// @Override
|
||||||
|
protected String getComponentName() {
|
||||||
|
return "undertow-keycloak";
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove all below this line after backport of https://issues.apache.org/jira/browse/CAMEL-12514 into fuse
|
||||||
|
@Override
|
||||||
|
public Consumer createConsumer(CamelContext camelContext, Processor processor, String verb, String basePath, String uriTemplate,
|
||||||
|
String consumes, String produces, RestConfiguration configuration, Map<String, Object> parameters) throws Exception {
|
||||||
|
return doCreateConsumer(camelContext, processor, verb, basePath, uriTemplate, consumes, produces, configuration, parameters, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Consumer createApiConsumer(CamelContext camelContext, Processor processor, String contextPath,
|
||||||
|
RestConfiguration configuration, Map<String, Object> parameters) throws Exception {
|
||||||
|
// reuse the createConsumer method we already have. The api need to use GET and match on uri prefix
|
||||||
|
return doCreateConsumer(camelContext, processor, "GET", contextPath, null, null, null, configuration, parameters, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Consumer doCreateConsumer(CamelContext camelContext, Processor processor, String verb, String basePath, String uriTemplate,
|
||||||
|
String consumes, String produces, RestConfiguration configuration, Map<String, Object> parameters, boolean api) throws Exception {
|
||||||
|
String path = basePath;
|
||||||
|
if (uriTemplate != null) {
|
||||||
|
// make sure to avoid double slashes
|
||||||
|
if (uriTemplate.startsWith("/")) {
|
||||||
|
path = path + uriTemplate;
|
||||||
|
} else {
|
||||||
|
path = path + "/" + uriTemplate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path = FileUtil.stripLeadingSeparator(path);
|
||||||
|
String scheme = "http";
|
||||||
|
String host = "";
|
||||||
|
int port = 0;
|
||||||
|
|
||||||
|
RestConfiguration config = configuration;
|
||||||
|
if (config == null) {
|
||||||
|
config = camelContext.getRestConfiguration(getComponentName(), true);
|
||||||
|
}
|
||||||
|
if (config.getScheme() != null) {
|
||||||
|
scheme = config.getScheme();
|
||||||
|
}
|
||||||
|
if (config.getHost() != null) {
|
||||||
|
host = config.getHost();
|
||||||
|
}
|
||||||
|
int num = config.getPort();
|
||||||
|
if (num > 0) {
|
||||||
|
port = num;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefix path with context-path if configured in rest-dsl configuration
|
||||||
|
String contextPath = config.getContextPath();
|
||||||
|
if (ObjectHelper.isNotEmpty(contextPath)) {
|
||||||
|
contextPath = FileUtil.stripTrailingSeparator(contextPath);
|
||||||
|
contextPath = FileUtil.stripLeadingSeparator(contextPath);
|
||||||
|
if (ObjectHelper.isNotEmpty(contextPath)) {
|
||||||
|
path = contextPath + "/" + path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no explicit hostname set then resolve the hostname
|
||||||
|
if (ObjectHelper.isEmpty(host)) {
|
||||||
|
if (config.getRestHostNameResolver() == RestConfiguration.RestHostNameResolver.allLocalIp) {
|
||||||
|
host = "0.0.0.0";
|
||||||
|
} else if (config.getRestHostNameResolver() == RestConfiguration.RestHostNameResolver.localHostName) {
|
||||||
|
host = HostUtils.getLocalHostName();
|
||||||
|
} else if (config.getRestHostNameResolver() == RestConfiguration.RestHostNameResolver.localIp) {
|
||||||
|
host = HostUtils.getLocalIp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> map = new HashMap<String, Object>();
|
||||||
|
// build query string, and append any endpoint configuration properties
|
||||||
|
if (config.getComponent() == null || config.getComponent().equals(getComponentName())) {
|
||||||
|
// setup endpoint options
|
||||||
|
if (config.getEndpointProperties() != null && !config.getEndpointProperties().isEmpty()) {
|
||||||
|
map.putAll(config.getEndpointProperties());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean explicitOptions = true;
|
||||||
|
// must use upper case for restrict
|
||||||
|
String restrict = verb.toUpperCase(Locale.US);
|
||||||
|
// allow OPTIONS in rest-dsl to allow clients to call the API and have responses with ALLOW headers
|
||||||
|
if (!restrict.contains("OPTIONS")) {
|
||||||
|
restrict += ",OPTIONS";
|
||||||
|
// this is not an explicit OPTIONS path in the rest-dsl
|
||||||
|
explicitOptions = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean cors = config.isEnableCORS();
|
||||||
|
if (cors) {
|
||||||
|
// allow HTTP Options as we want to handle CORS in rest-dsl
|
||||||
|
map.put("optionsEnabled", "true");
|
||||||
|
} else if (explicitOptions) {
|
||||||
|
// the rest-dsl is using OPTIONS
|
||||||
|
map.put("optionsEnabled", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
String query = URISupport.createQueryString(map);
|
||||||
|
|
||||||
|
String url;
|
||||||
|
if (api) {
|
||||||
|
url = getComponentName() + ":%s://%s:%s/%s?matchOnUriPrefix=true&httpMethodRestrict=%s";
|
||||||
|
} else {
|
||||||
|
url = getComponentName() + ":%s://%s:%s/%s?matchOnUriPrefix=false&httpMethodRestrict=%s";
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the endpoint
|
||||||
|
url = String.format(url, scheme, host, port, path, restrict);
|
||||||
|
|
||||||
|
if (!query.isEmpty()) {
|
||||||
|
url = url + "&" + query;
|
||||||
|
}
|
||||||
|
|
||||||
|
UndertowEndpoint endpoint = camelContext.getEndpoint(url, UndertowEndpoint.class);
|
||||||
|
setProperties(camelContext, endpoint, parameters);
|
||||||
|
|
||||||
|
if (!map.containsKey("undertowHttpBinding")) {
|
||||||
|
// use the rest binding, if not using a custom http binding
|
||||||
|
endpoint.setUndertowHttpBinding(new RestUndertowHttpBinding());
|
||||||
|
}
|
||||||
|
|
||||||
|
// configure consumer properties
|
||||||
|
Consumer consumer = endpoint.createConsumer(processor);
|
||||||
|
if (config.getConsumerProperties() != null && !config.getConsumerProperties().isEmpty()) {
|
||||||
|
setProperties(camelContext, consumer, config.getConsumerProperties());
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumer;
|
||||||
|
}
|
||||||
|
}
|
||||||
+219
@@ -0,0 +1,219 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.camel.undertow;
|
||||||
|
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.AuthenticatedActionsHandler;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.NodesRegistrationManagement;
|
||||||
|
import org.keycloak.adapters.PreAuthActionsHandler;
|
||||||
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
|
import org.keycloak.adapters.spi.AuthChallenge;
|
||||||
|
import org.keycloak.adapters.spi.AuthOutcome;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.adapters.spi.InMemorySessionIdMapper;
|
||||||
|
import org.keycloak.adapters.spi.SessionIdMapper;
|
||||||
|
import org.keycloak.adapters.undertow.KeycloakUndertowAccount;
|
||||||
|
import org.keycloak.adapters.undertow.OIDCUndertowHttpFacade;
|
||||||
|
import org.keycloak.adapters.undertow.SessionManagementBridge;
|
||||||
|
import org.keycloak.adapters.undertow.UndertowCookieTokenStore;
|
||||||
|
import org.keycloak.adapters.undertow.UndertowRequestAuthenticator;
|
||||||
|
import org.keycloak.adapters.undertow.UndertowSessionTokenStore;
|
||||||
|
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
|
||||||
|
import org.keycloak.enums.TokenStore;
|
||||||
|
import io.undertow.security.api.SecurityContext;
|
||||||
|
import io.undertow.security.idm.Account;
|
||||||
|
import io.undertow.security.idm.Credential;
|
||||||
|
import io.undertow.security.idm.IdentityManager;
|
||||||
|
import io.undertow.security.impl.SecurityContextImpl;
|
||||||
|
import io.undertow.server.HttpServerExchange;
|
||||||
|
import io.undertow.server.session.InMemorySessionManager;
|
||||||
|
import io.undertow.server.session.SessionManager;
|
||||||
|
import io.undertow.util.AttachmentKey;
|
||||||
|
import io.undertow.util.StatusCodes;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.apache.camel.Processor;
|
||||||
|
import org.apache.camel.component.undertow.UndertowConsumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class UndertowKeycloakConsumer extends UndertowConsumer {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(UndertowKeycloakConsumer.class.getName());
|
||||||
|
|
||||||
|
public static final AttachmentKey<KeycloakPrincipal> KEYCLOAK_PRINCIPAL_KEY = AttachmentKey.create(KeycloakPrincipal.class);
|
||||||
|
|
||||||
|
private static final IdentityManager IDENTITY_MANAGER = new IdentityManager() {
|
||||||
|
@Override
|
||||||
|
public Account verify(Account account) {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Account verify(String id, Credential credential) {
|
||||||
|
throw new IllegalStateException("Should never be called in Keycloak flow");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Account verify(Credential credential) {
|
||||||
|
throw new IllegalStateException("Should never be called in Keycloak flow");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
|
||||||
|
|
||||||
|
protected final NodesRegistrationManagement nodesRegistrationManagement = new NodesRegistrationManagement();
|
||||||
|
|
||||||
|
private final UndertowUserSessionManagement userSessionManagement = new UndertowUserSessionManagement();
|
||||||
|
|
||||||
|
protected final AdapterDeploymentContext deploymentContext;
|
||||||
|
|
||||||
|
protected final SessionManager sessionManager;
|
||||||
|
|
||||||
|
protected final List<String> allowedRoles;
|
||||||
|
|
||||||
|
private final int confidentialPort;
|
||||||
|
|
||||||
|
private final Pattern skipPattern;
|
||||||
|
|
||||||
|
public UndertowKeycloakConsumer(UndertowKeycloakEndpoint endpoint, Processor processor,
|
||||||
|
AdapterDeploymentContext deploymentContext, Pattern skipPattern, List<String> allowedRoles, int confidentialPort) {
|
||||||
|
super(endpoint, processor);
|
||||||
|
this.sessionManager = new InMemorySessionManager(endpoint.getEndpointUri());
|
||||||
|
this.deploymentContext = deploymentContext;
|
||||||
|
this.skipPattern = skipPattern;
|
||||||
|
this.confidentialPort = confidentialPort;
|
||||||
|
this.allowedRoles = allowedRoles == null ? Collections.<String>emptyList() : allowedRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getConfidentialPort() {
|
||||||
|
return confidentialPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleRequest(HttpServerExchange httpExchange) throws Exception {
|
||||||
|
if (shouldSkip(httpExchange.getRequestPath())) {
|
||||||
|
super.handleRequest(httpExchange);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//perform only non-blocking operation on exchange
|
||||||
|
if (httpExchange.isInIoThread()) {
|
||||||
|
httpExchange.dispatch(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OIDCUndertowHttpFacade facade = new OIDCUndertowHttpFacade(httpExchange);
|
||||||
|
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
|
||||||
|
|
||||||
|
if (deployment == null || !deployment.isConfigured()) {
|
||||||
|
httpExchange.setStatusCode(StatusCodes.FORBIDDEN);
|
||||||
|
LOG.fine("deployment not configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.fine("executing PreAuthActionsHandler");
|
||||||
|
SessionManagementBridge bridge = new SessionManagementBridge(userSessionManagement, sessionManager);
|
||||||
|
PreAuthActionsHandler preAuth = new PreAuthActionsHandler(bridge, deploymentContext, facade);
|
||||||
|
if (preAuth.handleRequest()) return;
|
||||||
|
|
||||||
|
SecurityContext securityContext = httpExchange.getSecurityContext();
|
||||||
|
if (securityContext == null) {
|
||||||
|
securityContext = new SecurityContextImpl(httpExchange, IDENTITY_MANAGER);
|
||||||
|
}
|
||||||
|
AdapterTokenStore tokenStore = getTokenStore(httpExchange, facade, deployment, securityContext);
|
||||||
|
tokenStore.checkCurrentToken();
|
||||||
|
|
||||||
|
LOG.fine("executing AuthenticatedActionsHandler");
|
||||||
|
RequestAuthenticator authenticator = new UndertowRequestAuthenticator(facade, deployment, confidentialPort, securityContext, httpExchange, tokenStore);
|
||||||
|
AuthOutcome outcome = authenticator.authenticate();
|
||||||
|
|
||||||
|
if (outcome == AuthOutcome.AUTHENTICATED) {
|
||||||
|
LOG.fine("AUTHENTICATED");
|
||||||
|
if (httpExchange.isResponseComplete()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AuthenticatedActionsHandler actions = new AuthenticatedActionsHandler(deployment, facade);
|
||||||
|
if (actions.handledRequest()) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
final Account authenticatedAccount = securityContext.getAuthenticatedAccount();
|
||||||
|
if (authenticatedAccount instanceof KeycloakUndertowAccount) {
|
||||||
|
final KeycloakUndertowAccount kua = (KeycloakUndertowAccount) authenticatedAccount;
|
||||||
|
httpExchange.putAttachment(KEYCLOAK_PRINCIPAL_KEY, (KeycloakPrincipal) kua.getPrincipal());
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> roles = Optional
|
||||||
|
.ofNullable(authenticatedAccount.getRoles())
|
||||||
|
.orElse((Set<String>) Collections.EMPTY_SET);
|
||||||
|
|
||||||
|
LOG.log(Level.FINE, "Allowed roles: {0}, current roles: {1}", new Object[] {allowedRoles, roles});
|
||||||
|
|
||||||
|
if (isRoleAllowed(roles, httpExchange)) {
|
||||||
|
super.handleRequest(httpExchange);
|
||||||
|
} else {
|
||||||
|
httpExchange.setStatusCode(StatusCodes.FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthChallenge challenge = authenticator.getChallenge();
|
||||||
|
if (challenge != null) {
|
||||||
|
LOG.fine("challenge");
|
||||||
|
challenge.challenge(facade);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpExchange.setStatusCode(StatusCodes.FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRoleAllowed(Set<String> roles, HttpServerExchange httpExchange) throws Exception {
|
||||||
|
for (String role : allowedRoles) {
|
||||||
|
if (roles.contains(role)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AdapterTokenStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, KeycloakDeployment deployment, SecurityContext securityContext) {
|
||||||
|
if (deployment.getTokenStore() == TokenStore.SESSION) {
|
||||||
|
return new UndertowSessionTokenStore(exchange, deployment, userSessionManagement, securityContext);
|
||||||
|
} else {
|
||||||
|
return new UndertowCookieTokenStore(facade, deployment, securityContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldSkip(String requestPath) {
|
||||||
|
return skipPattern != null && skipPattern.matcher(requestPath).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.camel.undertow;
|
||||||
|
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.KeycloakConfigResolver;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||||
|
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
|
import io.undertow.server.HttpServerExchange;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.apache.camel.Consumer;
|
||||||
|
import org.apache.camel.Exchange;
|
||||||
|
import org.apache.camel.Processor;
|
||||||
|
import org.apache.camel.component.undertow.UndertowComponent;
|
||||||
|
import org.apache.camel.component.undertow.UndertowEndpoint;
|
||||||
|
import static org.keycloak.adapters.camel.undertow.UndertowKeycloakConsumer.KEYCLOAK_PRINCIPAL_KEY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class UndertowKeycloakEndpoint extends UndertowEndpoint {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(UndertowKeycloakEndpoint.class.getName());
|
||||||
|
|
||||||
|
private KeycloakConfigResolver configResolver;
|
||||||
|
|
||||||
|
private AdapterConfig adapterConfig;
|
||||||
|
|
||||||
|
private String skipPattern;
|
||||||
|
|
||||||
|
private List<String> allowedRoles = Collections.emptyList();
|
||||||
|
|
||||||
|
private int confidentialPort = 8443;
|
||||||
|
|
||||||
|
public UndertowKeycloakEndpoint(String uri, UndertowComponent component) {
|
||||||
|
super(uri, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdapterConfig getAdapterConfig() {
|
||||||
|
return adapterConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdapterConfig(AdapterConfig adapterConfig) {
|
||||||
|
LOG.info("adapterConfig");
|
||||||
|
this.adapterConfig = adapterConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSkipPattern() {
|
||||||
|
return skipPattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSkipPattern(String skipPattern) {
|
||||||
|
this.skipPattern = skipPattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAllowedRoles() {
|
||||||
|
return allowedRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedRoles(List<String> allowedRoles) {
|
||||||
|
this.allowedRoles = allowedRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedRoles(String allowedRoles) {
|
||||||
|
this.allowedRoles = allowedRoles == null ? null : Arrays.asList(allowedRoles.split("\\s*,\\s*"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getConfidentialPort() {
|
||||||
|
return confidentialPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfidentialPort(int confidentialPort) {
|
||||||
|
this.confidentialPort = confidentialPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeycloakConfigResolver getConfigResolver() {
|
||||||
|
return configResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfigResolver(KeycloakConfigResolver configResolver) {
|
||||||
|
this.configResolver = configResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Consumer createConsumer(Processor processor) throws Exception {
|
||||||
|
return new UndertowKeycloakConsumer(this, processor, getDeploymentContext(), getSkipPatternAsPattern(), computeAllowedRoles(), this.confidentialPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> computeAllowedRoles() {
|
||||||
|
List<String> res = this.allowedRoles == null ? Collections.<String>emptyList() : this.allowedRoles;
|
||||||
|
if (res.isEmpty()) {
|
||||||
|
LOG.warning("No roles were configured, Keycloak will deny every request");
|
||||||
|
}
|
||||||
|
LOG.log(Level.FINE, "Allowed roles: {0}", res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Exchange createExchange(HttpServerExchange httpExchange) throws Exception {
|
||||||
|
final Exchange res = super.createExchange(httpExchange);
|
||||||
|
|
||||||
|
KeycloakPrincipal principal = httpExchange.getAttachment(KEYCLOAK_PRINCIPAL_KEY);
|
||||||
|
LOG.log(Level.FINE, "principal: {0}", principal);
|
||||||
|
if (principal != null) {
|
||||||
|
res.setProperty(KeycloakPrincipal.class.getName(), principal);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdapterDeploymentContext getDeploymentContext() {
|
||||||
|
if (configResolver != null) {
|
||||||
|
LOG.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolver.getClass());
|
||||||
|
return new AdapterDeploymentContext(configResolver);
|
||||||
|
} else if (adapterConfig != null) {
|
||||||
|
KeycloakDeployment kd = KeycloakDeploymentBuilder.build(adapterConfig);
|
||||||
|
return new AdapterDeploymentContext(kd);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.warning("Adapter is unconfigured, Keycloak will deny every request");
|
||||||
|
return new AdapterDeploymentContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pattern getSkipPatternAsPattern() {
|
||||||
|
return skipPattern == null
|
||||||
|
? null
|
||||||
|
: Pattern.compile(skipPattern, Pattern.DOTALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
class=org.keycloak.adapters.camel.undertow.UndertowKeycloakComponent
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-fuse7-integration-pom</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>4.0.0.Final</version>
|
||||||
|
<relativePath>../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-pax-web-jetty94</artifactId>
|
||||||
|
<name>Keycloak Fuse 7.0 Adapter - Jetty 9.4</name>
|
||||||
|
<packaging>bundle</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<keycloak.osgi.export>
|
||||||
|
org.keycloak.adapters.osgi.jetty94.*;version="${project.version}"
|
||||||
|
</keycloak.osgi.export>
|
||||||
|
<keycloak.osgi.import>
|
||||||
|
!org.keycloak.adapters.osgi.jetty94,
|
||||||
|
org.keycloak.*;version="${project.version}",
|
||||||
|
*;resolution:=optional
|
||||||
|
</keycloak.osgi.import>
|
||||||
|
<keycloak.osgi.fragment>org.ops4j.pax.web.pax-web-jetty</keycloak.osgi.fragment>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.logging</groupId>
|
||||||
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-api</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-spi</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.osgi</groupId>
|
||||||
|
<artifactId>org.osgi.core</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.osgi</groupId>
|
||||||
|
<artifactId>org.osgi.enterprise</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-jetty94-adapter</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-security</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<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-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>
|
||||||
|
<Fragment-Host>${keycloak.osgi.fragment}</Fragment-Host>
|
||||||
|
</instructions>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.osgi.jetty94;
|
||||||
|
|
||||||
|
import org.ops4j.pax.web.service.AuthenticatorService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class KeycloakAuthenticatorService implements AuthenticatorService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T getAuthenticatorService(String method, Class<T> iface) {
|
||||||
|
if (method == null || iface != org.eclipse.jetty.security.Authenticator.class) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("KEYCLOAK".equalsIgnoreCase(method)) {
|
||||||
|
return iface.cast(new org.keycloak.adapters.jetty.KeycloakJettyAuthenticator());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+280
@@ -0,0 +1,280 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.osgi.jetty94;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.security.ConstraintMapping;
|
||||||
|
import org.eclipse.jetty.util.security.Constraint;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.ops4j.pax.web.service.WebContainer;
|
||||||
|
import org.ops4j.pax.web.service.spi.model.SecurityConstraintMappingModel;
|
||||||
|
import org.osgi.framework.BundleContext;
|
||||||
|
import org.osgi.framework.ServiceReference;
|
||||||
|
import org.osgi.service.http.HttpContext;
|
||||||
|
import org.osgi.util.tracker.ServiceTracker;
|
||||||
|
import org.osgi.util.tracker.ServiceTrackerCustomizer;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration with pax-web, which allows to inject custom jetty-web.xml configuration from current bundle classpath into {@link WebContainer}
|
||||||
|
* and allows to inject custom security constraint for securing resources by Keycloak.
|
||||||
|
*
|
||||||
|
* <p>It assumes that pax-web {@link WebContainer} is used as implementation of OSGI {@link org.osgi.service.http.HttpService}, which
|
||||||
|
* is true in karaf/fuse environment</p>
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class PaxWebIntegrationService {
|
||||||
|
|
||||||
|
protected static final Logger log = Logger.getLogger(PaxWebIntegrationService.class);
|
||||||
|
|
||||||
|
private BundleContext bundleContext;
|
||||||
|
private String jettyWebXmlLocation;
|
||||||
|
private List<Object> constraintMappings;
|
||||||
|
|
||||||
|
private ServiceTracker webContainerTracker;
|
||||||
|
private HttpContext httpContext;
|
||||||
|
|
||||||
|
public BundleContext getBundleContext() {
|
||||||
|
return bundleContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBundleContext(BundleContext bundleContext) {
|
||||||
|
this.bundleContext = bundleContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getJettyWebXmlLocation() {
|
||||||
|
return jettyWebXmlLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJettyWebXmlLocation(String jettyWebXmlLocation) {
|
||||||
|
this.jettyWebXmlLocation = jettyWebXmlLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Object> getConstraintMappings() {
|
||||||
|
return constraintMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConstraintMappings(List<Object> constraintMappings) {
|
||||||
|
this.constraintMappings = constraintMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ServiceTracker getWebContainerTracker() {
|
||||||
|
return webContainerTracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected HttpContext getHttpContext() {
|
||||||
|
return httpContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHttpContext(HttpContext httpContext) {
|
||||||
|
this.httpContext = httpContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
ServiceTrackerCustomizer trackerCustomizer = new ServiceTrackerCustomizer() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object addingService(ServiceReference reference) {
|
||||||
|
return addingWebContainerCallback(reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modifiedService(ServiceReference reference, Object service) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removedService(ServiceReference reference, Object service) {
|
||||||
|
removingWebContainerCallback(reference);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
webContainerTracker = new ServiceTracker(bundleContext, WebContainer.class.getName(), trackerCustomizer);
|
||||||
|
webContainerTracker.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
webContainerTracker.remove(webContainerTracker.getServiceReference());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected WebContainer addingWebContainerCallback(ServiceReference webContainerServiceReference) {
|
||||||
|
WebContainer service = (WebContainer) bundleContext.getService(webContainerServiceReference);
|
||||||
|
if (httpContext == null) {
|
||||||
|
httpContext = service.createDefaultHttpContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
addJettyWebXml(service);
|
||||||
|
|
||||||
|
if (constraintMappings == null) {
|
||||||
|
throw new IllegalStateException("constraintMappings was null!");
|
||||||
|
}
|
||||||
|
List<ConstraintHandler> handlers = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
handlers.add(new JettyConstraintHandler());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
handlers.add(new PaxWebConstraintHandler());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
for (Object constraintMapping : constraintMappings) {
|
||||||
|
boolean handled = false;
|
||||||
|
for (ConstraintHandler handler : handlers) {
|
||||||
|
handled |= handler.addConstraintMapping(httpContext, service, constraintMapping);
|
||||||
|
}
|
||||||
|
if (!handled) {
|
||||||
|
log.warnv("Unable to add constraint mapping for constraint of type " + constraintMapping.getClass().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service.registerLoginConfig("BASIC", "does-not-matter", null, null, httpContext);
|
||||||
|
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addJettyWebXml(WebContainer service) {
|
||||||
|
String jettyWebXmlLoc;
|
||||||
|
if (this.jettyWebXmlLocation == null) {
|
||||||
|
jettyWebXmlLoc = "/WEB-INF/jetty-web.xml";
|
||||||
|
} else {
|
||||||
|
jettyWebXmlLoc = this.jettyWebXmlLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
URL jettyWebXml = bundleContext.getBundle().getResource(jettyWebXmlLoc);
|
||||||
|
if (jettyWebXml != null) {
|
||||||
|
log.debug("Found jetty-web XML configuration on bundle classpath on " + jettyWebXmlLoc);
|
||||||
|
service.registerJettyWebXml(jettyWebXml, httpContext);
|
||||||
|
} else {
|
||||||
|
log.debug("Not found jetty-web XML configuration on bundle classpath on " + jettyWebXmlLoc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addConstraintMapping(WebContainer service, SecurityConstraintMappingModel constraintMapping) {
|
||||||
|
String name = constraintMapping.getConstraintName();
|
||||||
|
if (name == null) {
|
||||||
|
name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getUrl() + ", dataConstraint=" + constraintMapping.getDataConstraint() + ", canAuthenticate="
|
||||||
|
+ constraintMapping.isAuthentication() + ", roles=" + constraintMapping.getRoles());
|
||||||
|
service.registerConstraintMapping(name, constraintMapping.getUrl(), constraintMapping.getMapping(), constraintMapping.getDataConstraint(), constraintMapping.isAuthentication(), constraintMapping.getRoles(), httpContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addConstraintMapping(WebContainer service, ConstraintMapping constraintMapping) {
|
||||||
|
Constraint constraint = constraintMapping.getConstraint();
|
||||||
|
String[] roles = constraint.getRoles();
|
||||||
|
// name property is unavailable on constraint object :/
|
||||||
|
|
||||||
|
String name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
|
||||||
|
int dataConstraint = constraint.getDataConstraint();
|
||||||
|
String dataConstraintStr;
|
||||||
|
switch (dataConstraint) {
|
||||||
|
case Constraint.DC_UNSET: dataConstraintStr = null; break;
|
||||||
|
case Constraint.DC_NONE: dataConstraintStr = "NONE"; break;
|
||||||
|
case Constraint.DC_CONFIDENTIAL: dataConstraintStr = "CONFIDENTIAL"; break;
|
||||||
|
case Constraint.DC_INTEGRAL: dataConstraintStr = "INTEGRAL"; break;
|
||||||
|
default:
|
||||||
|
log.warnv("Unknown data constraint: " + dataConstraint);
|
||||||
|
dataConstraintStr = "CONFIDENTIAL";
|
||||||
|
}
|
||||||
|
List<String> rolesList = Arrays.asList(roles);
|
||||||
|
|
||||||
|
log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getPathSpec() + ", dataConstraint=" + dataConstraintStr + ", canAuthenticate="
|
||||||
|
+ constraint.getAuthenticate() + ", roles=" + rolesList);
|
||||||
|
service.registerConstraintMapping(name, constraintMapping.getPathSpec(), null, dataConstraintStr, constraint.getAuthenticate(), rolesList, httpContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void removingWebContainerCallback(ServiceReference serviceReference) {
|
||||||
|
WebContainer service = (WebContainer)bundleContext.getService(serviceReference);
|
||||||
|
if (service != null) {
|
||||||
|
service.unregisterLoginConfig(httpContext);
|
||||||
|
service.unregisterConstraintMapping(httpContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ConstraintHandler {
|
||||||
|
boolean addConstraintMapping(HttpContext httpContext, WebContainer service, Object cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PaxWebConstraintHandler implements ConstraintHandler {
|
||||||
|
|
||||||
|
public boolean addConstraintMapping(HttpContext httpContext, WebContainer service, Object cm) {
|
||||||
|
if (cm instanceof SecurityConstraintMappingModel) {
|
||||||
|
SecurityConstraintMappingModel constraintMapping = (SecurityConstraintMappingModel) cm;
|
||||||
|
String name = constraintMapping.getConstraintName();
|
||||||
|
if (name == null) {
|
||||||
|
name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getUrl() + ", dataConstraint=" + constraintMapping.getDataConstraint() + ", canAuthenticate="
|
||||||
|
+ constraintMapping.isAuthentication() + ", roles=" + constraintMapping.getRoles());
|
||||||
|
service.registerConstraintMapping(name, constraintMapping.getUrl(), constraintMapping.getMapping(), constraintMapping.getDataConstraint(), constraintMapping.isAuthentication(), constraintMapping.getRoles(), httpContext);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class JettyConstraintHandler implements ConstraintHandler {
|
||||||
|
|
||||||
|
public boolean addConstraintMapping(HttpContext httpContext, WebContainer service, Object cm) {
|
||||||
|
if (cm instanceof ConstraintMapping) {
|
||||||
|
ConstraintMapping constraintMapping = (ConstraintMapping) cm;
|
||||||
|
Constraint constraint = constraintMapping.getConstraint();
|
||||||
|
String[] roles = constraint.getRoles();
|
||||||
|
// name property is unavailable on constraint object :/
|
||||||
|
|
||||||
|
String name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
|
||||||
|
int dataConstraint = constraint.getDataConstraint();
|
||||||
|
String dataConstraintStr;
|
||||||
|
switch (dataConstraint) {
|
||||||
|
case Constraint.DC_UNSET:
|
||||||
|
dataConstraintStr = null;
|
||||||
|
break;
|
||||||
|
case Constraint.DC_NONE:
|
||||||
|
dataConstraintStr = "NONE";
|
||||||
|
break;
|
||||||
|
case Constraint.DC_CONFIDENTIAL:
|
||||||
|
dataConstraintStr = "CONFIDENTIAL";
|
||||||
|
break;
|
||||||
|
case Constraint.DC_INTEGRAL:
|
||||||
|
dataConstraintStr = "INTEGRAL";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.warnv("Unknown data constraint: " + dataConstraint);
|
||||||
|
dataConstraintStr = "CONFIDENTIAL";
|
||||||
|
}
|
||||||
|
List<String> rolesList = Arrays.asList(roles);
|
||||||
|
|
||||||
|
log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getPathSpec() + ", dataConstraint=" + dataConstraintStr + ", canAuthenticate="
|
||||||
|
+ constraint.getAuthenticate() + ", roles=" + rolesList);
|
||||||
|
service.registerConstraintMapping(name, constraintMapping.getPathSpec(), null, dataConstraintStr, constraint.getAuthenticate(), rolesList, httpContext);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
org.keycloak.adapters.osgi.jetty94.KeycloakAuthenticatorService
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-parent</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>4.0.0.Final</version>
|
||||||
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-fuse7-integration-pom</artifactId>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
<name>Keycloak Fuse 7.0 Integration</name>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<fuse7.version>7.0.1-SNAPSHOT</fuse7.version>
|
||||||
|
<tomcat.version>8.0.14</tomcat.version>
|
||||||
|
<jetty9.version>9.4.2.v20170220</jetty9.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>camel-undertow</module>
|
||||||
|
<module>jetty94</module>
|
||||||
|
<module>tomcat8</module>
|
||||||
|
<module>undertow</module>
|
||||||
|
</modules>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-fuse7-integration-pom</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>4.0.0.Final</version>
|
||||||
|
<relativePath>../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-pax-web-tomcat8</artifactId>
|
||||||
|
<name>Keycloak Fuse 7.0 Adapter - Tomcat 8</name>
|
||||||
|
<packaging>bundle</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<keycloak.osgi.export>
|
||||||
|
org.keycloak.adapters.osgi.tomcat.*;version="${project.version}"
|
||||||
|
</keycloak.osgi.export>
|
||||||
|
<keycloak.osgi.import>
|
||||||
|
!org.keycloak.adapters.osgi.tomcat,
|
||||||
|
org.keycloak.*;version="${project.version}",
|
||||||
|
*;resolution:=optional
|
||||||
|
</keycloak.osgi.import>
|
||||||
|
<keycloak.osgi.fragment>org.ops4j.pax.web.pax-web-tomcat</keycloak.osgi.fragment>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.logging</groupId>
|
||||||
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-api</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-tomcat8-adapter</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.tomcat</groupId>
|
||||||
|
<artifactId>tomcat-catalina</artifactId>
|
||||||
|
<version>${tomcat.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<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-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>
|
||||||
|
<Fragment-Host>${keycloak.osgi.fragment}</Fragment-Host>
|
||||||
|
</instructions>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+19
-9
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
* and other contributors as indicated by the @author tags.
|
* and other contributors as indicated by the @author tags.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@@ -14,18 +14,28 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.adapter.example.authorization;
|
|
||||||
|
|
||||||
import org.jboss.arquillian.container.test.api.RunAsClient;
|
package org.keycloak.adapters.osgi.tomcat;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
|
|
||||||
|
import org.ops4j.pax.web.service.AuthenticatorService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author tkyjovsk
|
* @author hmlnarik
|
||||||
*/
|
*/
|
||||||
@RunAsClient
|
public class KeycloakAuthenticatorService implements AuthenticatorService {
|
||||||
@AppServerContainer("app-server-eap")
|
|
||||||
//@AdapterLibsLocationProperty("adapter.libs.wildfly")
|
@Override
|
||||||
public class EAPServletAuthzAdapterTest extends AbstractServletAuthzFunctionalAdapterTest {
|
public <T> T getAuthenticatorService(String method, Class<T> iface) {
|
||||||
|
if (method == null || iface != org.apache.catalina.Valve.class) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("KEYCLOAK".equalsIgnoreCase(method)) {
|
||||||
|
return iface.cast(new org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
org.keycloak.adapters.osgi.tomcat.KeycloakAuthenticatorService
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-fuse7-integration-pom</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>4.0.0.Final</version>
|
||||||
|
<relativePath>../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-pax-web-undertow</artifactId>
|
||||||
|
<name>Keycloak Fuse 7.0 Adapter - Undertow</name>
|
||||||
|
<packaging>bundle</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<keycloak.osgi.export>
|
||||||
|
org.keycloak.adapters.osgi.undertow.*;version="${project.version}"
|
||||||
|
</keycloak.osgi.export>
|
||||||
|
<keycloak.osgi.import>
|
||||||
|
!org.keycloak.adapters.osgi.undertow,
|
||||||
|
org.keycloak.*;version="${project.version}",
|
||||||
|
org.keycloak.adapters.osgi.*;version="${project.version}",
|
||||||
|
org.ops4j.pax.web.*;version="[3.0,8)",
|
||||||
|
javax.servlet.*;version="[2.5,4)";resolution:=optional,
|
||||||
|
org.apache.cxf.transport.http;resolution:=optional;version="[3,4)",
|
||||||
|
org.apache.cxf.transport.http_undertow;resolution:=optional;version="[3,4)",
|
||||||
|
org.apache.cxf.transport.servlet;resolution:=optional;version="[3,4)",
|
||||||
|
io.undertow.*,
|
||||||
|
*;resolution:=optional
|
||||||
|
</keycloak.osgi.import>
|
||||||
|
<keycloak.osgi.fragment>org.ops4j.pax.web.pax-web-undertow</keycloak.osgi.fragment>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.logging</groupId>
|
||||||
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.osgi</groupId>
|
||||||
|
<artifactId>org.osgi.core</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.osgi</groupId>
|
||||||
|
<artifactId>org.osgi.enterprise</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-runtime</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-spi</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-api</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.cxf</groupId>
|
||||||
|
<artifactId>cxf-rt-transports-http</artifactId>
|
||||||
|
<version>${cxf.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.cxf</groupId>
|
||||||
|
<artifactId>cxf-rt-transports-http-undertow</artifactId>
|
||||||
|
<version>${cxf.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.undertow</groupId>
|
||||||
|
<artifactId>undertow-servlet</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-undertow-adapter</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-osgi-adapter</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<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-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>
|
||||||
|
<Fragment-Host>${keycloak.osgi.fragment}</Fragment-Host>
|
||||||
|
</instructions>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+174
@@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.osgi.undertow;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.KeycloakConfigResolver;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||||
|
import org.keycloak.adapters.NodesRegistrationManagement;
|
||||||
|
import org.keycloak.adapters.spi.InMemorySessionIdMapper;
|
||||||
|
import org.keycloak.adapters.spi.SessionIdMapper;
|
||||||
|
import org.keycloak.adapters.undertow.UndertowAuthenticationMechanism;
|
||||||
|
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
|
||||||
|
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
|
import io.undertow.security.api.AuthenticationMechanism;
|
||||||
|
import io.undertow.security.api.AuthenticationMode;
|
||||||
|
import io.undertow.security.handlers.AuthenticationCallHandler;
|
||||||
|
import io.undertow.security.handlers.AuthenticationConstraintHandler;
|
||||||
|
import io.undertow.security.handlers.AuthenticationMechanismsHandler;
|
||||||
|
import io.undertow.security.handlers.SecurityInitialHandler;
|
||||||
|
import io.undertow.security.idm.Account;
|
||||||
|
import io.undertow.security.idm.Credential;
|
||||||
|
import io.undertow.security.idm.IdentityManager;
|
||||||
|
import io.undertow.server.HttpHandler;
|
||||||
|
import io.undertow.server.HttpServerExchange;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.apache.cxf.transport.http_undertow.CXFUndertowHttpHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class CxfKeycloakAuthHandler implements CXFUndertowHttpHandler {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(CxfKeycloakAuthHandler.class.getName());
|
||||||
|
|
||||||
|
private static final IdentityManager IDENTITY_MANAGER = new IdentityManager() {
|
||||||
|
@Override
|
||||||
|
public Account verify(Account account) {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Account verify(String id, Credential credential) {
|
||||||
|
throw new IllegalStateException("Should never be called in Keycloak flow");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Account verify(Credential credential) {
|
||||||
|
throw new IllegalStateException("Should never be called in Keycloak flow");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final UndertowUserSessionManagement userSessionManagement = new UndertowUserSessionManagement();
|
||||||
|
|
||||||
|
protected final NodesRegistrationManagement nodesRegistrationManagement = new NodesRegistrationManagement();
|
||||||
|
|
||||||
|
protected final SessionIdMapper idMapper = new InMemorySessionIdMapper();
|
||||||
|
|
||||||
|
private final AtomicReference<HttpHandler> securityHandler = new AtomicReference<>();
|
||||||
|
|
||||||
|
private Pattern skipPattern;
|
||||||
|
|
||||||
|
private int confidentialPort = 8443;
|
||||||
|
|
||||||
|
private HttpHandler next;
|
||||||
|
|
||||||
|
private KeycloakConfigResolver configResolver;
|
||||||
|
|
||||||
|
private AdapterConfig adapterConfig;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleRequest(HttpServerExchange exchange) throws Exception {
|
||||||
|
if (shouldSkip(exchange.getRequestPath())) {
|
||||||
|
next.handleRequest(exchange);
|
||||||
|
} else {
|
||||||
|
getSecurityHandler().handleRequest(exchange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpHandler getSecurityHandler() {
|
||||||
|
if (this.securityHandler.get() == null) {
|
||||||
|
HttpHandler handler = this.next;
|
||||||
|
|
||||||
|
handler = new AuthenticationCallHandler(handler);
|
||||||
|
handler = new AuthenticationConstraintHandler(handler);
|
||||||
|
|
||||||
|
AdapterDeploymentContext deploymentContext = buildDeploymentContext();
|
||||||
|
|
||||||
|
final List<AuthenticationMechanism> mechanisms
|
||||||
|
= Collections.<AuthenticationMechanism>singletonList(
|
||||||
|
new UndertowAuthenticationMechanism(deploymentContext, userSessionManagement, nodesRegistrationManagement, confidentialPort, null));
|
||||||
|
handler = new AuthenticationMechanismsHandler(handler, mechanisms);
|
||||||
|
|
||||||
|
this.securityHandler.compareAndSet(null, new SecurityInitialHandler(AuthenticationMode.PRO_ACTIVE, IDENTITY_MANAGER, "KEYCLOAK", handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.securityHandler.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdapterDeploymentContext buildDeploymentContext() {
|
||||||
|
if (configResolver != null) {
|
||||||
|
LOG.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolver.getClass());
|
||||||
|
return new AdapterDeploymentContext(configResolver);
|
||||||
|
} else if (adapterConfig != null) {
|
||||||
|
KeycloakDeployment kd = KeycloakDeploymentBuilder.build(adapterConfig);
|
||||||
|
return new AdapterDeploymentContext(kd);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.warning("Adapter is unconfigured, Keycloak will deny every request");
|
||||||
|
return new AdapterDeploymentContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setNext(HttpHandler nextHandler) {
|
||||||
|
this.next = nextHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldSkip(String requestPath) {
|
||||||
|
return skipPattern != null && skipPattern.matcher(requestPath).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeycloakConfigResolver getConfigResolver() {
|
||||||
|
return configResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfigResolver(KeycloakConfigResolver configResolver) {
|
||||||
|
this.configResolver = configResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getConfidentialPort() {
|
||||||
|
return confidentialPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfidentialPort(int confidentialPort) {
|
||||||
|
this.confidentialPort = confidentialPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdapterConfig getAdapterConfig() {
|
||||||
|
return adapterConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdapterConfig(AdapterConfig adapterConfig) {
|
||||||
|
this.adapterConfig = adapterConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSkipPattern() {
|
||||||
|
return skipPattern.pattern();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSkipPattern(String skipPattern) {
|
||||||
|
this.skipPattern = Pattern.compile(skipPattern, Pattern.DOTALL);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.osgi.undertow;
|
||||||
|
|
||||||
|
import org.ops4j.pax.web.service.AuthenticatorService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class KeycloakAuthenticatorService implements AuthenticatorService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T getAuthenticatorService(String method, Class<T> iface) {
|
||||||
|
if (method == null || iface != io.undertow.servlet.ServletExtension.class) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("KEYCLOAK".equalsIgnoreCase(method)) {
|
||||||
|
return iface.cast(new org.keycloak.adapters.undertow.KeycloakServletExtension());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.osgi.undertow;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.osgi.PaxWebSecurityConstraintMapping;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.ops4j.pax.web.service.WebContainer;
|
||||||
|
import org.osgi.framework.BundleContext;
|
||||||
|
import org.osgi.framework.ServiceReference;
|
||||||
|
import org.osgi.service.http.HttpContext;
|
||||||
|
import org.osgi.util.tracker.ServiceTracker;
|
||||||
|
import org.osgi.util.tracker.ServiceTrackerCustomizer;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration with pax-web, which allows to inject custom security constraint for securing resources by Keycloak.
|
||||||
|
*
|
||||||
|
* <p>It assumes that pax-web {@link WebContainer} is used as implementation of OSGI {@link org.osgi.service.http.HttpService}, which
|
||||||
|
* is true in karaf/fuse environment</p>
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class PaxWebIntegrationService {
|
||||||
|
|
||||||
|
protected static final Logger log = Logger.getLogger(PaxWebIntegrationService.class);
|
||||||
|
|
||||||
|
private BundleContext bundleContext;
|
||||||
|
private List<PaxWebSecurityConstraintMapping> constraintMappings;
|
||||||
|
|
||||||
|
private ServiceTracker webContainerTracker;
|
||||||
|
private HttpContext httpContext;
|
||||||
|
|
||||||
|
public BundleContext getBundleContext() {
|
||||||
|
return bundleContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBundleContext(BundleContext bundleContext) {
|
||||||
|
this.bundleContext = bundleContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PaxWebSecurityConstraintMapping> getConstraintMappings() {
|
||||||
|
return constraintMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConstraintMappings(List<PaxWebSecurityConstraintMapping> constraintMappings) {
|
||||||
|
this.constraintMappings = constraintMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ServiceTracker getWebContainerTracker() {
|
||||||
|
return webContainerTracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected HttpContext getHttpContext() {
|
||||||
|
return httpContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHttpContext(HttpContext httpContext) {
|
||||||
|
this.httpContext = httpContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
ServiceTrackerCustomizer trackerCustomizer = new ServiceTrackerCustomizer() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object addingService(ServiceReference reference) {
|
||||||
|
return addingWebContainerCallback(reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modifiedService(ServiceReference reference, Object service) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removedService(ServiceReference reference, Object service) {
|
||||||
|
removingWebContainerCallback(reference);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
webContainerTracker = new ServiceTracker(bundleContext, WebContainer.class.getName(), trackerCustomizer);
|
||||||
|
webContainerTracker.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
webContainerTracker.remove(webContainerTracker.getServiceReference());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected WebContainer addingWebContainerCallback(ServiceReference webContainerServiceReference) {
|
||||||
|
WebContainer service = (WebContainer) bundleContext.getService(webContainerServiceReference);
|
||||||
|
if (httpContext == null) {
|
||||||
|
httpContext = service.createDefaultHttpContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraintMappings == null) {
|
||||||
|
throw new IllegalStateException("constraintMappings was null!");
|
||||||
|
}
|
||||||
|
for (PaxWebSecurityConstraintMapping constraintMapping : constraintMappings) {
|
||||||
|
addConstraintMapping(service, constraintMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
service.registerLoginConfig("KEYCLOAK", "specified-in-keycloak-json", null, null, httpContext);
|
||||||
|
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addConstraintMapping(WebContainer service, PaxWebSecurityConstraintMapping cm) {
|
||||||
|
log.debug("Adding security constraint name=" + cm.getConstraintName() + ", url=" + cm.getUrl() + ", dataConstraint=" + cm.getDataConstraint() + ", canAuthenticate="
|
||||||
|
+ cm.isAuthentication() + ", roles=" + cm.getRoles());
|
||||||
|
service.registerConstraintMapping(
|
||||||
|
cm.getConstraintName(),
|
||||||
|
cm.getMapping(),
|
||||||
|
cm.getUrl(),
|
||||||
|
cm.getDataConstraint(),
|
||||||
|
cm.isAuthentication(),
|
||||||
|
cm.getRoles(),
|
||||||
|
httpContext
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void removingWebContainerCallback(ServiceReference serviceReference) {
|
||||||
|
WebContainer service = (WebContainer)bundleContext.getService(serviceReference);
|
||||||
|
if (service != null) {
|
||||||
|
service.unregisterLoginConfig(httpContext);
|
||||||
|
service.unregisterConstraintMapping(httpContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
org.keycloak.adapters.osgi.undertow.KeycloakAuthenticatorService
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
@@ -63,6 +63,15 @@
|
|||||||
<groupId>org.jboss.logging</groupId>
|
<groupId>org.jboss.logging</groupId>
|
||||||
<artifactId>jboss-logging</artifactId>
|
<artifactId>jboss-logging</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-client</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.spec.javax.ws.rs</groupId>
|
||||||
|
<artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
+702
@@ -0,0 +1,702 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.installed;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||||
|
import org.keycloak.adapters.ServerRequest;
|
||||||
|
import org.keycloak.common.util.Base64;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.jose.jwe.*;
|
||||||
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
|
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
|
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.SecretKeyFactory;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import javax.ws.rs.client.Client;
|
||||||
|
import javax.ws.rs.client.Entity;
|
||||||
|
import javax.ws.rs.client.WebTarget;
|
||||||
|
import javax.ws.rs.core.Form;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.KeySpec;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All kcinit commands that take input ask for
|
||||||
|
* <p>
|
||||||
|
* 1. . kcinit
|
||||||
|
* - setup and export KC_SESSION_KEY env var if not set.
|
||||||
|
* - checks to see if master token valid, refresh is possible, exit if token valid
|
||||||
|
* - performs command line login
|
||||||
|
* - stores master token for master client
|
||||||
|
* 2. app.sh is a wrapper for app cli.
|
||||||
|
* - token=`kcinit token app`
|
||||||
|
* - checks to see if token for app client has been fetched, refresh if valid, output token to sys.out if exists
|
||||||
|
* - if no token, login. Prompts go to stderr.
|
||||||
|
* - pass token as cmd line param to app or as environment variable.
|
||||||
|
* <p>
|
||||||
|
* 3. kcinit password {password}
|
||||||
|
* - outputs password key that is used for encryption.
|
||||||
|
* - can be used in .bashrc as export KC_SESSSION_KEY=`kcinit password {password}` or just set it in .bat file
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class KcinitDriver {
|
||||||
|
|
||||||
|
public static final String KC_SESSION_KEY = "KC_SESSION_KEY";
|
||||||
|
public static final String KC_LOGIN_CONFIG_PATH = "KC_LOGIN_CONFIG_PATH";
|
||||||
|
protected Map<String, String> config;
|
||||||
|
protected boolean debug = true;
|
||||||
|
|
||||||
|
protected static byte[] salt = new byte[]{-4, 88, 66, -101, 78, -94, 21, 105};
|
||||||
|
|
||||||
|
String[] args = null;
|
||||||
|
|
||||||
|
protected boolean forceLogin;
|
||||||
|
protected boolean browserLogin;
|
||||||
|
|
||||||
|
public void mainCmd(String[] args) throws Exception {
|
||||||
|
|
||||||
|
this.args = args;
|
||||||
|
|
||||||
|
|
||||||
|
if (args.length == 0) {
|
||||||
|
printHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args[0].equalsIgnoreCase("token")) {
|
||||||
|
//System.err.println("executing token");
|
||||||
|
token();
|
||||||
|
} else if (args[0].equalsIgnoreCase("login")) {
|
||||||
|
login();
|
||||||
|
} else if (args[0].equalsIgnoreCase("logout")) {
|
||||||
|
logout();
|
||||||
|
} else if (args[0].equalsIgnoreCase("env")) {
|
||||||
|
System.out.println(System.getenv().toString());
|
||||||
|
} else if (args[0].equalsIgnoreCase("install")) {
|
||||||
|
install();
|
||||||
|
} else if (args[0].equalsIgnoreCase("uninstall")) {
|
||||||
|
uninstall();
|
||||||
|
} else if (args[0].equalsIgnoreCase("password")) {
|
||||||
|
passwordKey();
|
||||||
|
} else {
|
||||||
|
KeycloakInstalled.console().writer().println("Unknown command: " + args[0]);
|
||||||
|
KeycloakInstalled.console().writer().println();
|
||||||
|
printHelp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHome() {
|
||||||
|
String home = System.getenv("HOME");
|
||||||
|
if (home == null) {
|
||||||
|
home = System.getProperty("HOME");
|
||||||
|
if (home == null) {
|
||||||
|
home = Paths.get("").toAbsolutePath().normalize().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return home;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void passwordKey() {
|
||||||
|
if (args.length < 2) {
|
||||||
|
printHelp();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
String password = args[1];
|
||||||
|
try {
|
||||||
|
String encodedKey = generateEncryptionKey(password);
|
||||||
|
System.out.printf(encodedKey);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String generateEncryptionKey(String password) throws NoSuchAlgorithmException, InvalidKeySpecException {
|
||||||
|
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||||
|
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 100, 128);
|
||||||
|
SecretKey tmp = factory.generateSecret(spec);
|
||||||
|
byte[] aeskey = tmp.getEncoded();
|
||||||
|
return Base64.encodeBytes(aeskey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWE createJWE() {
|
||||||
|
String key = getEncryptionKey();
|
||||||
|
if (key == null) {
|
||||||
|
throw new RuntimeException(KC_SESSION_KEY + " env var not set");
|
||||||
|
}
|
||||||
|
byte[] aesKey = null;
|
||||||
|
try {
|
||||||
|
aesKey = Base64.decode(key.getBytes("UTF-8"));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("invalid " + KC_SESSION_KEY + "env var");
|
||||||
|
}
|
||||||
|
|
||||||
|
JWE jwe = new JWE();
|
||||||
|
final SecretKey aesSecret = new SecretKeySpec(aesKey, "AES");
|
||||||
|
jwe.getKeyStorage()
|
||||||
|
.setEncryptionKey(aesSecret);
|
||||||
|
return jwe;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String encryptionKey;
|
||||||
|
|
||||||
|
protected String getEncryptionKey() {
|
||||||
|
if (encryptionKey != null) return encryptionKey;
|
||||||
|
return System.getenv(KC_SESSION_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String encrypt(String payload) {
|
||||||
|
JWE jwe = createJWE();
|
||||||
|
JWEHeader jweHeader = new JWEHeader(JWEConstants.A128KW, JWEConstants.A128CBC_HS256, null);
|
||||||
|
try {
|
||||||
|
jwe.header(jweHeader).content(payload.getBytes("UTF-8"));
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("cannot encode payload as UTF-8");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return jwe.encodeJwe();
|
||||||
|
} catch (JWEException e) {
|
||||||
|
throw new RuntimeException("cannot encrypt payload", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String decrypt(String encoded) {
|
||||||
|
JWE jwe = createJWE();
|
||||||
|
try {
|
||||||
|
jwe.verifyAndDecodeJwe(encoded);
|
||||||
|
byte[] content = jwe.getContent();
|
||||||
|
if (content == null) return null;
|
||||||
|
return new String(content, "UTF-8");
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new RuntimeException("cannot decrypt payload", ex);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getenv(String name, String defaultValue) {
|
||||||
|
String val = System.getenv(name);
|
||||||
|
return val == null ? defaultValue : val;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getConfigDirectory() {
|
||||||
|
return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit").toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public File getConfigFile() {
|
||||||
|
return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit", "config.json").toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getTokenFilePath(String client) {
|
||||||
|
return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit", "tokens", client).toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getTokenDirectory() {
|
||||||
|
return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit", "tokens").toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean encrypted = false;
|
||||||
|
|
||||||
|
protected void checkEnv() {
|
||||||
|
File configFile = getConfigFile();
|
||||||
|
if (!configFile.exists()) {
|
||||||
|
KeycloakInstalled.console().writer().println("You have not configured kcinit. Please run 'kcinit install' to configure.");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
byte[] data = new byte[0];
|
||||||
|
try {
|
||||||
|
data = readFileRaw(configFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
if (data == null) {
|
||||||
|
KeycloakInstalled.console().writer().println("Config file unreadable. Please run 'kcinit install' to configure.");
|
||||||
|
System.exit(1);
|
||||||
|
|
||||||
|
}
|
||||||
|
String encodedJwe = null;
|
||||||
|
try {
|
||||||
|
encodedJwe = new String(data, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encodedJwe.contains("realm")) {
|
||||||
|
encrypted = false;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
encrypted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (System.getenv(KC_SESSION_KEY) == null) {
|
||||||
|
promptLocalPassword();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void promptLocalPassword() {
|
||||||
|
String password = KeycloakInstalled.console().passwordPrompt("Enter password to unlock kcinit config files: ");
|
||||||
|
try {
|
||||||
|
encryptionKey = generateEncryptionKey(password);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected String readFile(File fp) {
|
||||||
|
try {
|
||||||
|
byte[] data = readFileRaw(fp);
|
||||||
|
if (data == null) return null;
|
||||||
|
String file = new String(data, "UTF-8");
|
||||||
|
if (!encrypted) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
String decrypted = decrypt(file);
|
||||||
|
if (decrypted == null)
|
||||||
|
throw new RuntimeException("Unable to decrypt file. Did you set your local password correctly?");
|
||||||
|
return decrypted;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("failed to decrypt file: " + fp.getAbsolutePath() + " Did you set your local password correctly?", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] readFileRaw(File fp) throws IOException {
|
||||||
|
if (!fp.exists()) return null;
|
||||||
|
FileInputStream fis = new FileInputStream(fp);
|
||||||
|
byte[] data = new byte[(int) fp.length()];
|
||||||
|
fis.read(data);
|
||||||
|
fis.close();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void writeFile(File fp, String payload) {
|
||||||
|
try {
|
||||||
|
String data = payload;
|
||||||
|
if (encrypted) data = encrypt(payload);
|
||||||
|
FileOutputStream fos = new FileOutputStream(fp);
|
||||||
|
fos.write(data.getBytes("UTF-8"));
|
||||||
|
fos.flush();
|
||||||
|
fos.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void install() {
|
||||||
|
if (getEncryptionKey() == null) {
|
||||||
|
if (KeycloakInstalled.console().confirm("Do you want to protect tokens stored locally with a password? (y/n): ")) {
|
||||||
|
String password = "p";
|
||||||
|
String confirm = "c";
|
||||||
|
do {
|
||||||
|
password = KeycloakInstalled.console().passwordPrompt("Enter local password: ");
|
||||||
|
confirm = KeycloakInstalled.console().passwordPrompt("Confirm local password: ");
|
||||||
|
if (!password.equals(confirm)) {
|
||||||
|
KeycloakInstalled.console().writer().println();
|
||||||
|
KeycloakInstalled.console().writer().println("Confirmation does not match. Try again.");
|
||||||
|
KeycloakInstalled.console().writer().println();
|
||||||
|
}
|
||||||
|
} while (!password.equals(confirm));
|
||||||
|
try {
|
||||||
|
this.encrypted = true;
|
||||||
|
this.encryptionKey = generateEncryptionKey(password);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!KeycloakInstalled.console().confirm("KC_SESSION_KEY env var already set. Do you want to use this as your local encryption key? (y/n): ")) {
|
||||||
|
KeycloakInstalled.console().writer().println("Unset KC_SESSION_KEY env var and run again");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
this.encrypted = true;
|
||||||
|
this.encryptionKey = getEncryptionKey();
|
||||||
|
}
|
||||||
|
String server = KeycloakInstalled.console().readLine("Authentication server URL [http://localhost:8080/auth]: ").trim();
|
||||||
|
String realm = KeycloakInstalled.console().readLine("Name of realm [master]: ").trim();
|
||||||
|
String client = KeycloakInstalled.console().readLine("CLI client id [kcinit]: ").trim();
|
||||||
|
String secret = KeycloakInstalled.console().readLine("CLI client secret [none]: ").trim();
|
||||||
|
if (server.equals("")) {
|
||||||
|
server = "http://localhost:8080/auth";
|
||||||
|
}
|
||||||
|
if (realm.equals("")) {
|
||||||
|
realm = "master";
|
||||||
|
}
|
||||||
|
if (client.equals("")) {
|
||||||
|
client = "kcinit";
|
||||||
|
}
|
||||||
|
File configDir = getTokenDirectory();
|
||||||
|
configDir.mkdirs();
|
||||||
|
|
||||||
|
File configFile = getConfigFile();
|
||||||
|
Map<String, String> props = new HashMap<>();
|
||||||
|
props.put("server", server);
|
||||||
|
props.put("realm", realm);
|
||||||
|
props.put("client", client);
|
||||||
|
props.put("secret", secret);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String json = JsonSerialization.writeValueAsString(props);
|
||||||
|
writeFile(configFile, json);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
KeycloakInstalled.console().writer().println();
|
||||||
|
KeycloakInstalled.console().writer().println("Installation complete!");
|
||||||
|
KeycloakInstalled.console().writer().println();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void printHelp() {
|
||||||
|
KeycloakInstalled.console().writer().println("Commands:");
|
||||||
|
KeycloakInstalled.console().writer().println(" login [-f] -f forces login");
|
||||||
|
KeycloakInstalled.console().writer().println(" logout");
|
||||||
|
KeycloakInstalled.console().writer().println(" token [client] - print access token of desired client. Defaults to default master client. Will print either 'error', 'not-allowed', or 'login-required' on error.");
|
||||||
|
KeycloakInstalled.console().writer().println(" install - Install this utility. Will store in $HOME/.keycloak/kcinit unless " + KC_LOGIN_CONFIG_PATH + " env var is set");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public AdapterConfig getConfig() {
|
||||||
|
File configFile = getConfigFile();
|
||||||
|
if (!configFile.exists()) {
|
||||||
|
KeycloakInstalled.console().writer().println("You have not configured kcinit. Please run 'kcinit install' to configure.");
|
||||||
|
System.exit(1);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AdapterConfig config = new AdapterConfig();
|
||||||
|
config.setAuthServerUrl((String) getConfigProperties().get("server"));
|
||||||
|
config.setRealm((String) getConfigProperties().get("realm"));
|
||||||
|
config.setResource((String) getConfigProperties().get("client"));
|
||||||
|
config.setSslRequired("external");
|
||||||
|
String secret = (String) getConfigProperties().get("secret");
|
||||||
|
if (secret != null && !secret.trim().equals("")) {
|
||||||
|
Map<String, Object> creds = new HashMap<>();
|
||||||
|
creds.put("secret", secret);
|
||||||
|
config.setCredentials(creds);
|
||||||
|
} else {
|
||||||
|
config.setPublicClient(true);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> getConfigProperties() {
|
||||||
|
if (this.config != null) return this.config;
|
||||||
|
if (!getConfigFile().exists()) {
|
||||||
|
KeycloakInstalled.console().writer().println();
|
||||||
|
KeycloakInstalled.console().writer().println(("Config file does not exist. Run kcinit install to set it up."));
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
String json = readFile(getConfigFile());
|
||||||
|
try {
|
||||||
|
Map map = JsonSerialization.readValue(json, Map.class);
|
||||||
|
config = (Map<String, String>) map;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return this.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String readToken(String client) throws Exception {
|
||||||
|
String json = getTokenResponse(client);
|
||||||
|
if (json == null) return null;
|
||||||
|
|
||||||
|
|
||||||
|
if (json != null) {
|
||||||
|
try {
|
||||||
|
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
|
||||||
|
if (Time.currentTime() < tokenResponse.getExpiresIn()) {
|
||||||
|
return tokenResponse.getToken();
|
||||||
|
}
|
||||||
|
AdapterConfig config = getConfig();
|
||||||
|
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
|
||||||
|
installed.refreshToken(tokenResponse.getRefreshToken());
|
||||||
|
processResponse(installed, client);
|
||||||
|
return tokenResponse.getToken();
|
||||||
|
} catch (Exception e) {
|
||||||
|
File tokenFile = getTokenFilePath(client);
|
||||||
|
if (tokenFile.exists()) {
|
||||||
|
tokenFile.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public String readRefreshToken(String client) throws Exception {
|
||||||
|
String json = getTokenResponse(client);
|
||||||
|
if (json == null) return null;
|
||||||
|
|
||||||
|
|
||||||
|
if (json != null) {
|
||||||
|
try {
|
||||||
|
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
|
||||||
|
return tokenResponse.getRefreshToken();
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (debug) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
File tokenFile = getTokenFilePath(client);
|
||||||
|
if (tokenFile.exists()) {
|
||||||
|
tokenFile.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String getTokenResponse(String client) throws IOException {
|
||||||
|
File tokenFile = getTokenFilePath(client);
|
||||||
|
try {
|
||||||
|
return readFile(tokenFile);
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (debug) {
|
||||||
|
System.err.println("Failed to read encrypted file");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
if (tokenFile.exists()) tokenFile.delete();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void token() throws Exception {
|
||||||
|
KeycloakInstalled.console().stderrOutput();
|
||||||
|
|
||||||
|
checkEnv();
|
||||||
|
String masterClient = getMasterClient();
|
||||||
|
String client = masterClient;
|
||||||
|
if (args.length > 1) {
|
||||||
|
client = args[1];
|
||||||
|
}
|
||||||
|
//System.err.println("readToken: " + client);
|
||||||
|
String token = readToken(client);
|
||||||
|
if (token != null) {
|
||||||
|
System.out.print(token);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (token == null && client.equals(masterClient)) {
|
||||||
|
//System.err.println("not logged in, logging in.");
|
||||||
|
doConsoleLogin();
|
||||||
|
token = readToken(client);
|
||||||
|
if (token != null) {
|
||||||
|
System.out.print(token);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
String masterToken = readToken(masterClient);
|
||||||
|
if (masterToken == null) {
|
||||||
|
//System.err.println("not logged in, logging in.");
|
||||||
|
doConsoleLogin();
|
||||||
|
masterToken = readToken(masterClient);
|
||||||
|
if (masterToken == null) {
|
||||||
|
System.err.println("Login failed. Cannot retrieve token");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//System.err.println("exchange: " + client);
|
||||||
|
Client httpClient = getHttpClient();
|
||||||
|
|
||||||
|
WebTarget exchangeUrl = httpClient.target(getServer())
|
||||||
|
.path("/realms")
|
||||||
|
.path(getRealm())
|
||||||
|
.path("protocol/openid-connect/token");
|
||||||
|
|
||||||
|
Form form = new Form()
|
||||||
|
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
|
||||||
|
.param(OAuth2Constants.CLIENT_ID, masterClient)
|
||||||
|
.param(OAuth2Constants.SUBJECT_TOKEN, masterToken)
|
||||||
|
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
|
||||||
|
.param(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.REFRESH_TOKEN_TYPE)
|
||||||
|
.param(OAuth2Constants.AUDIENCE, client);
|
||||||
|
if (getMasterClientSecret() != null) {
|
||||||
|
form.param(OAuth2Constants.CLIENT_SECRET, getMasterClientSecret());
|
||||||
|
}
|
||||||
|
Response response = exchangeUrl.request().post(Entity.form(
|
||||||
|
form
|
||||||
|
));
|
||||||
|
|
||||||
|
if (response.getStatus() == 401 || response.getStatus() == 403) {
|
||||||
|
response.close();
|
||||||
|
System.err.println("Not allowed to exchange for client token");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.getStatus() != 200) {
|
||||||
|
if (response.getMediaType() != null && response.getMediaType().equals(MediaType.APPLICATION_JSON_TYPE)) {
|
||||||
|
try {
|
||||||
|
String json = response.readEntity(String.class);
|
||||||
|
OAuth2ErrorRepresentation error = JsonSerialization.readValue(json, OAuth2ErrorRepresentation.class);
|
||||||
|
System.err.println("Failed to exchange token: " + error.getError() + ". " + error.getErrorDescription());
|
||||||
|
System.exit(1);
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
ignore.printStackTrace();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.close();
|
||||||
|
System.err.println("Unknown error exchanging for client token: " + response.getStatus());
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String json = response.readEntity(String.class);
|
||||||
|
response.close();
|
||||||
|
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
|
||||||
|
if (tokenResponse.getToken() != null) {
|
||||||
|
getTokenDirectory().mkdirs();
|
||||||
|
tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
|
||||||
|
tokenResponse.setIdToken(null);
|
||||||
|
json = JsonSerialization.writeValueAsString(tokenResponse);
|
||||||
|
writeFile(getTokenFilePath(client), json);
|
||||||
|
System.out.printf(tokenResponse.getToken());
|
||||||
|
} else {
|
||||||
|
System.err.println("Error processing token");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getMasterClientSecret() {
|
||||||
|
return getProperty("secret");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getServer() {
|
||||||
|
return getProperty("server");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getRealm() {
|
||||||
|
return getProperty("realm");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProperty(String name) {
|
||||||
|
return (String) getConfigProperties().get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean forceLogin() {
|
||||||
|
return args.length > 0 && args[0].equals("-f");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public Client getHttpClient() {
|
||||||
|
return new ResteasyClientBuilder().disableTrustManager().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void login() throws Exception {
|
||||||
|
checkEnv();
|
||||||
|
this.args = Arrays.copyOfRange(this.args, 1, this.args.length);
|
||||||
|
for (String arg : args) {
|
||||||
|
if (arg.equals("-f") || arg.equals("-force")) {
|
||||||
|
forceLogin = true;
|
||||||
|
this.args = Arrays.copyOfRange(this.args, 1, this.args.length);
|
||||||
|
} else if (arg.equals("-browser") || arg.equals("-b")) {
|
||||||
|
browserLogin = true;
|
||||||
|
this.args = Arrays.copyOfRange(this.args, 1, this.args.length);
|
||||||
|
} else {
|
||||||
|
System.err.println("Illegal argument: " + arg);
|
||||||
|
printHelp();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String masterClient = getMasterClient();
|
||||||
|
if (!forceLogin && readToken(masterClient) != null) {
|
||||||
|
KeycloakInstalled.console().writer().println("Already logged in. `kcinit -f` to force relogin");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doConsoleLogin();
|
||||||
|
KeycloakInstalled.console().writer().println("Login successful!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void doConsoleLogin() throws Exception {
|
||||||
|
String masterClient = getMasterClient();
|
||||||
|
AdapterConfig config = getConfig();
|
||||||
|
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
|
||||||
|
//System.err.println("calling loginCommandLine");
|
||||||
|
if (!installed.loginCommandLine()) {
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
processResponse(installed, masterClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMasterClient() {
|
||||||
|
return getProperty("client");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processResponse(KeycloakInstalled installed, String client) throws IOException {
|
||||||
|
AccessTokenResponse tokenResponse = installed.getTokenResponse();
|
||||||
|
tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
|
||||||
|
tokenResponse.setIdToken(null);
|
||||||
|
String json = JsonSerialization.writeValueAsString(tokenResponse);
|
||||||
|
getTokenDirectory().mkdirs();
|
||||||
|
writeFile(getTokenFilePath(client), json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void logout() throws Exception {
|
||||||
|
String token = readRefreshToken(getMasterClient());
|
||||||
|
if (token != null) {
|
||||||
|
try {
|
||||||
|
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getConfig());
|
||||||
|
ServerRequest.invokeLogout(deployment, token);
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (debug) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if (getTokenDirectory().exists()) {
|
||||||
|
for (File fp : getTokenDirectory().listFiles()) fp.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void uninstall() throws Exception {
|
||||||
|
File configFile = getConfigFile();
|
||||||
|
if (configFile.exists()) configFile.delete();
|
||||||
|
if (getTokenDirectory().exists()) {
|
||||||
|
for (File fp : getTokenDirectory().listFiles()) fp.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-266
@@ -1,266 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
|
||||||
* and other contributors as indicated by the @author tags.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.keycloak.adapters.installed;
|
|
||||||
|
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
|
||||||
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
|
||||||
import org.keycloak.adapters.ServerRequest;
|
|
||||||
import org.keycloak.common.util.Time;
|
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
|
||||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
|
||||||
import org.keycloak.util.JsonSerialization;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
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 KeycloakCliSso {
|
|
||||||
|
|
||||||
public void mainCmd(String[] args) throws Exception {
|
|
||||||
if (args.length != 1) {
|
|
||||||
printHelp();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args[0].equalsIgnoreCase("login")) {
|
|
||||||
login();
|
|
||||||
} else if (args[0].equalsIgnoreCase("login-manual")) {
|
|
||||||
loginManual();
|
|
||||||
} else if (args[0].equalsIgnoreCase("token")) {
|
|
||||||
token();
|
|
||||||
} else if (args[0].equalsIgnoreCase("logout")) {
|
|
||||||
logout();
|
|
||||||
} else if (args[0].equalsIgnoreCase("env")) {
|
|
||||||
System.out.println(System.getenv().toString());
|
|
||||||
} else {
|
|
||||||
printHelp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void printHelp() {
|
|
||||||
System.err.println("Commands:");
|
|
||||||
System.err.println(" login - login with desktop browser if available, otherwise do manual login. Output is access token.");
|
|
||||||
System.err.println(" login-manual - manual login");
|
|
||||||
System.err.println(" token - print access token if logged in");
|
|
||||||
System.err.println(" logout - logout.");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AdapterConfig getConfig() {
|
|
||||||
String url = System.getProperty("KEYCLOAK_AUTH_SERVER");
|
|
||||||
if (url == null) {
|
|
||||||
System.err.println("KEYCLOAK_AUTH_SERVER property not set");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
String realm = System.getProperty("KEYCLOAK_REALM");
|
|
||||||
if (realm == null) {
|
|
||||||
System.err.println("KEYCLOAK_REALM property not set");
|
|
||||||
System.exit(1);
|
|
||||||
|
|
||||||
}
|
|
||||||
String client = System.getProperty("KEYCLOAK_CLIENT");
|
|
||||||
if (client == null) {
|
|
||||||
System.err.println("KEYCLOAK_CLIENT property not set");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
String secret = System.getProperty("KEYCLOAK_CLIENT_SECRET");
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
AdapterConfig config = new AdapterConfig();
|
|
||||||
config.setAuthServerUrl(url);
|
|
||||||
config.setRealm(realm);
|
|
||||||
config.setResource(client);
|
|
||||||
config.setSslRequired("external");
|
|
||||||
if (secret != null) {
|
|
||||||
Map<String, Object> creds = new HashMap<>();
|
|
||||||
creds.put("secret", secret);
|
|
||||||
config.setCredentials(creds);
|
|
||||||
} else {
|
|
||||||
config.setPublicClient(true);
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean checkToken() throws Exception {
|
|
||||||
String token = getTokenResponse();
|
|
||||||
if (token == null) return false;
|
|
||||||
|
|
||||||
|
|
||||||
if (token != null) {
|
|
||||||
Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token);
|
|
||||||
if (m.find()) {
|
|
||||||
String json = m.group(0);
|
|
||||||
try {
|
|
||||||
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
|
|
||||||
if (Time.currentTime() < tokenResponse.getExpiresIn()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
AdapterConfig config = getConfig();
|
|
||||||
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
|
|
||||||
installed.refreshToken(tokenResponse.getRefreshToken());
|
|
||||||
processResponse(installed);
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("Error processing existing token");
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getTokenResponse() throws IOException {
|
|
||||||
String token = null;
|
|
||||||
File tokenFile = getTokenFilePath();
|
|
||||||
if (tokenFile.exists()) {
|
|
||||||
FileInputStream fis = new FileInputStream(tokenFile);
|
|
||||||
byte[] data = new byte[(int) tokenFile.length()];
|
|
||||||
fis.read(data);
|
|
||||||
fis.close();
|
|
||||||
token = new String(data, "UTF-8");
|
|
||||||
}
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void token() throws Exception {
|
|
||||||
String token = getTokenResponse();
|
|
||||||
if (token == null) {
|
|
||||||
System.err.println("There is no token for client");
|
|
||||||
System.exit(1);
|
|
||||||
} else {
|
|
||||||
Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token);
|
|
||||||
if (m.find()) {
|
|
||||||
String json = m.group(0);
|
|
||||||
try {
|
|
||||||
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
|
|
||||||
if (Time.currentTime() < tokenResponse.getExpiresIn()) {
|
|
||||||
System.out.println(tokenResponse.getToken());
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
System.err.println("token in response file is expired");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("Failure processing token response file");
|
|
||||||
e.printStackTrace();
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
System.err.println("Could not find json within token response file");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void login() throws Exception {
|
|
||||||
if (checkToken()) return;
|
|
||||||
AdapterConfig config = getConfig();
|
|
||||||
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
|
|
||||||
installed.login();
|
|
||||||
processResponse(installed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getHome() {
|
|
||||||
String home = System.getenv("HOME");
|
|
||||||
if (home == null) {
|
|
||||||
home = System.getProperty("HOME");
|
|
||||||
if (home == null) {
|
|
||||||
home = Paths.get("").toAbsolutePath().normalize().toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return home;
|
|
||||||
}
|
|
||||||
|
|
||||||
public File getTokenDirectory() {
|
|
||||||
return Paths.get(getHome(), System.getProperty("basepath", ".keycloak-sso"), System.getProperty("KEYCLOAK_REALM")).toFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
public File getTokenFilePath() {
|
|
||||||
return Paths.get(getHome(), System.getProperty("basepath", ".keycloak-sso"), System.getProperty("KEYCLOAK_REALM"), System.getProperty("KEYCLOAK_CLIENT") + ".json").toFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processResponse(KeycloakInstalled installed) throws IOException {
|
|
||||||
AccessTokenResponse tokenResponse = installed.getTokenResponse();
|
|
||||||
tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
|
|
||||||
tokenResponse.setIdToken(null);
|
|
||||||
String output = JsonSerialization.writeValueAsString(tokenResponse);
|
|
||||||
getTokenDirectory().mkdirs();
|
|
||||||
FileOutputStream fos = new FileOutputStream(getTokenFilePath());
|
|
||||||
fos.write(output.getBytes("UTF-8"));
|
|
||||||
fos.flush();
|
|
||||||
fos.close();
|
|
||||||
System.out.println(tokenResponse.getToken());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loginManual() throws Exception {
|
|
||||||
if (checkToken()) return;
|
|
||||||
AdapterConfig config = getConfig();
|
|
||||||
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
|
|
||||||
KeycloakInstalled installed = new KeycloakInstalled(deployment);
|
|
||||||
installed.loginManual();
|
|
||||||
processResponse(installed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void logout() throws Exception {
|
|
||||||
String token = getTokenResponse();
|
|
||||||
if (token != null) {
|
|
||||||
Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token);
|
|
||||||
if (m.find()) {
|
|
||||||
String json = m.group(0);
|
|
||||||
try {
|
|
||||||
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
|
|
||||||
if (Time.currentTime() > tokenResponse.getExpiresIn()) {
|
|
||||||
System.err.println("Login is expired");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
AdapterConfig config = getConfig();
|
|
||||||
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
|
|
||||||
ServerRequest.invokeLogout(deployment, tokenResponse.getRefreshToken());
|
|
||||||
for (File fp : getTokenDirectory().listFiles()) fp.delete();
|
|
||||||
System.out.println("logout complete");
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("Failure processing token response file");
|
|
||||||
e.printStackTrace();
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
System.err.println("Could not find json within token response file");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
System.err.println("Not logged in");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+305
-74
@@ -17,7 +17,8 @@
|
|||||||
|
|
||||||
package org.keycloak.adapters.installed;
|
package org.keycloak.adapters.installed;
|
||||||
|
|
||||||
import org.apache.commons.codec.Charsets;
|
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
|
||||||
|
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.OAuthErrorException;
|
import org.keycloak.OAuthErrorException;
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
@@ -32,15 +33,13 @@ import org.keycloak.representations.AccessToken;
|
|||||||
import org.keycloak.representations.AccessTokenResponse;
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
|
|
||||||
|
import javax.ws.rs.client.Client;
|
||||||
|
import javax.ws.rs.client.Entity;
|
||||||
|
import javax.ws.rs.core.Form;
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.BufferedReader;
|
import java.io.*;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.PrintStream;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.io.Reader;
|
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@@ -48,6 +47,8 @@ import java.net.URISyntaxException;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
@@ -56,6 +57,7 @@ public class KeycloakInstalled {
|
|||||||
|
|
||||||
public interface HttpResponseWriter {
|
public interface HttpResponseWriter {
|
||||||
void success(PrintWriter pw, KeycloakInstalled ki);
|
void success(PrintWriter pw, KeycloakInstalled ki);
|
||||||
|
|
||||||
void failure(PrintWriter pw, KeycloakInstalled ki);
|
void failure(PrintWriter pw, KeycloakInstalled ki);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +79,10 @@ public class KeycloakInstalled {
|
|||||||
private Locale locale;
|
private Locale locale;
|
||||||
private HttpResponseWriter loginResponseWriter;
|
private HttpResponseWriter loginResponseWriter;
|
||||||
private HttpResponseWriter logoutResponseWriter;
|
private HttpResponseWriter logoutResponseWriter;
|
||||||
|
private ResteasyClient resteasyClient;
|
||||||
|
Pattern callbackPattern = Pattern.compile("callback\\s*=\\s*\"([^\"]+)\"");
|
||||||
|
Pattern paramPattern = Pattern.compile("param=\"([^\"]+)\"\\s+label=\"([^\"]+)\"\\s+mask=(\\S+)");
|
||||||
|
Pattern codePattern = Pattern.compile("code=([^&]+)");
|
||||||
|
|
||||||
|
|
||||||
public KeycloakInstalled() {
|
public KeycloakInstalled() {
|
||||||
@@ -93,70 +98,12 @@ public class KeycloakInstalled {
|
|||||||
this.deployment = deployment;
|
this.deployment = deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpResponseWriter defaultLoginWriter = new HttpResponseWriter() {
|
|
||||||
@Override
|
|
||||||
public void success(PrintWriter pw, KeycloakInstalled ki) {
|
|
||||||
pw.println("HTTP/1.1 200 OK");
|
|
||||||
pw.println("Content-Type: text/html");
|
|
||||||
pw.println();
|
|
||||||
pw.println("<html><h1>Login completed.</h1><div>");
|
|
||||||
pw.println("This browser will remain logged in until you close it, logout, or the session expires.");
|
|
||||||
pw.println("</div></html>");
|
|
||||||
pw.flush();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void failure(PrintWriter pw, KeycloakInstalled ki) {
|
|
||||||
pw.println("HTTP/1.1 200 OK");
|
|
||||||
pw.println("Content-Type: text/html");
|
|
||||||
pw.println();
|
|
||||||
pw.println("<html><h1>Login attempt failed.</h1><div>");
|
|
||||||
pw.println("</div></html>");
|
|
||||||
pw.flush();
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static HttpResponseWriter defaultLogoutWriter = new HttpResponseWriter() {
|
|
||||||
@Override
|
|
||||||
public void success(PrintWriter pw, KeycloakInstalled ki) {
|
|
||||||
pw.println("HTTP/1.1 200 OK");
|
|
||||||
pw.println("Content-Type: text/html");
|
|
||||||
pw.println();
|
|
||||||
pw.println("<html><h1>Logout completed.</h1><div>");
|
|
||||||
pw.println("You may close this browser tab.");
|
|
||||||
pw.println("</div></html>");
|
|
||||||
pw.flush();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void failure(PrintWriter pw, KeycloakInstalled ki) {
|
|
||||||
pw.println("HTTP/1.1 200 OK");
|
|
||||||
pw.println("Content-Type: text/html");
|
|
||||||
pw.println();
|
|
||||||
pw.println("<html><h1>Logout failed.</h1><div>");
|
|
||||||
pw.println("You may close this browser tab.");
|
|
||||||
pw.println("</div></html>");
|
|
||||||
pw.flush();
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public HttpResponseWriter getLoginResponseWriter() {
|
public HttpResponseWriter getLoginResponseWriter() {
|
||||||
if (loginResponseWriter == null) {
|
return null;
|
||||||
return defaultLoginWriter;
|
|
||||||
} else {
|
|
||||||
return loginResponseWriter;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpResponseWriter getLogoutResponseWriter() {
|
public HttpResponseWriter getLogoutResponseWriter() {
|
||||||
if (logoutResponseWriter == null) {
|
return null;
|
||||||
return defaultLogoutWriter;
|
|
||||||
} else {
|
|
||||||
return logoutResponseWriter;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLoginResponseWriter(HttpResponseWriter loginResponseWriter) {
|
public void setLoginResponseWriter(HttpResponseWriter loginResponseWriter) {
|
||||||
@@ -167,6 +114,10 @@ public class KeycloakInstalled {
|
|||||||
this.logoutResponseWriter = logoutResponseWriter;
|
this.logoutResponseWriter = logoutResponseWriter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setResteasyClient(ResteasyClient resteasyClient) {
|
||||||
|
this.resteasyClient = resteasyClient;
|
||||||
|
}
|
||||||
|
|
||||||
public Locale getLocale() {
|
public Locale getLocale() {
|
||||||
return locale;
|
return locale;
|
||||||
}
|
}
|
||||||
@@ -290,6 +241,272 @@ public class KeycloakInstalled {
|
|||||||
status = Status.LOGGED_MANUAL;
|
status = Status.LOGGED_MANUAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class Console {
|
||||||
|
protected java.io.Console console = System.console();
|
||||||
|
protected PrintWriter writer;
|
||||||
|
protected BufferedReader reader;
|
||||||
|
|
||||||
|
static Console SINGLETON = new Console();
|
||||||
|
|
||||||
|
private Console() {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public PrintWriter writer() {
|
||||||
|
if (console == null) {
|
||||||
|
if (writer == null) {
|
||||||
|
writer = new PrintWriter(System.err, true);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
return console.writer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Reader reader() {
|
||||||
|
if (console == null) {
|
||||||
|
return getReader();
|
||||||
|
}
|
||||||
|
return console.reader();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected BufferedReader getReader() {
|
||||||
|
if (reader != null) return reader;
|
||||||
|
reader = new BufferedReader(new BufferedReader(new InputStreamReader(System.in)));
|
||||||
|
return reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Console format(String fmt, Object... args) {
|
||||||
|
if (console == null) {
|
||||||
|
writer().format(fmt, args);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
console.format(fmt, args);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Console printf(String format, Object... args) {
|
||||||
|
if (console == null) {
|
||||||
|
writer().printf(format, args);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
console.printf(format, args);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String readLine(String fmt, Object... args) {
|
||||||
|
if (console == null) {
|
||||||
|
format(fmt, args);
|
||||||
|
return readLine();
|
||||||
|
}
|
||||||
|
return console.readLine(fmt, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean confirm(String fmt, Object... args) {
|
||||||
|
String prompt = "";
|
||||||
|
while (!"y".equals(prompt) && !"n".equals(prompt)) {
|
||||||
|
prompt = readLine(fmt, args);
|
||||||
|
}
|
||||||
|
return "y".equals(prompt);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public String prompt(String fmt, Object... args) {
|
||||||
|
String prompt = "";
|
||||||
|
while (prompt.equals("")) {
|
||||||
|
prompt = readLine(fmt, args).trim();
|
||||||
|
}
|
||||||
|
return prompt;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public String passwordPrompt(String fmt, Object... args) {
|
||||||
|
String prompt = "";
|
||||||
|
while (prompt.equals("")) {
|
||||||
|
char[] val = readPassword(fmt, args);
|
||||||
|
prompt = new String(val);
|
||||||
|
prompt = prompt.trim();
|
||||||
|
}
|
||||||
|
return prompt;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public String readLine() {
|
||||||
|
if (console == null) {
|
||||||
|
try {
|
||||||
|
return getReader().readLine();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return console.readLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
public char[] readPassword(String fmt, Object... args) {
|
||||||
|
if (console == null) {
|
||||||
|
return readLine(fmt, args).toCharArray();
|
||||||
|
|
||||||
|
}
|
||||||
|
return console.readPassword(fmt, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public char[] readPassword() {
|
||||||
|
if (console == null) {
|
||||||
|
return readLine().toCharArray();
|
||||||
|
}
|
||||||
|
return console.readPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void flush() {
|
||||||
|
if (console == null) {
|
||||||
|
System.err.flush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stderrOutput() {
|
||||||
|
//System.err.println("not using System.console()");
|
||||||
|
console = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Console console() {
|
||||||
|
return Console.SINGLETON;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean loginCommandLine() throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||||
|
String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
|
||||||
|
|
||||||
|
return loginCommandLine(redirectUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Experimental proprietary WWW-Authentication challenge protocol.
|
||||||
|
* WWW-Authentication: X-Text-Form-Challenge callback="{url}" param="{param-name}" label="{param-display-label}"
|
||||||
|
*
|
||||||
|
* @param redirectUri
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
* @throws ServerRequest.HttpFailure
|
||||||
|
* @throws VerificationException
|
||||||
|
*/
|
||||||
|
public boolean loginCommandLine(String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||||
|
String authUrl = deployment.getAuthUrl().clone()
|
||||||
|
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
||||||
|
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
|
||||||
|
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
|
||||||
|
.queryParam("display", "console")
|
||||||
|
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
|
||||||
|
.build().toString();
|
||||||
|
ResteasyClient client = createResteasyClient();
|
||||||
|
try {
|
||||||
|
//System.err.println("initial request");
|
||||||
|
Response response = client.target(authUrl).request().get();
|
||||||
|
while (true) {
|
||||||
|
if (response.getStatus() == 403) {
|
||||||
|
if (response.getMediaType() != null) {
|
||||||
|
String splash = response.readEntity(String.class);
|
||||||
|
console().writer().println(splash);
|
||||||
|
} else {
|
||||||
|
System.err.println("Forbidden to login");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else if (response.getStatus() == 401) {
|
||||||
|
String authenticationHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
|
||||||
|
if (authenticationHeader == null) {
|
||||||
|
System.err.println("Failure: Invalid protocol. No WWW-Authenticate header");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//System.err.println("got header: " + authenticationHeader);
|
||||||
|
if (!authenticationHeader.contains("X-Text-Form-Challenge")) {
|
||||||
|
System.err.println("Failure: Invalid WWW-Authenticate header.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (response.getMediaType() != null) {
|
||||||
|
String splash = response.readEntity(String.class);
|
||||||
|
console().writer().println(splash);
|
||||||
|
} else {
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
Matcher m = callbackPattern.matcher(authenticationHeader);
|
||||||
|
if (!m.find()) {
|
||||||
|
System.err.println("Failure: Invalid WWW-Authenticate header.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String callback = m.group(1);
|
||||||
|
//System.err.println("callback: " + callback);
|
||||||
|
m = paramPattern.matcher(authenticationHeader);
|
||||||
|
Form form = new Form();
|
||||||
|
while (m.find()) {
|
||||||
|
String param = m.group(1);
|
||||||
|
String label = m.group(2);
|
||||||
|
String mask = m.group(3).trim();
|
||||||
|
boolean maskInput = mask.equals("true");
|
||||||
|
String value = null;
|
||||||
|
if (maskInput) {
|
||||||
|
char[] txt = console().readPassword(label);
|
||||||
|
value = new String(txt);
|
||||||
|
} else {
|
||||||
|
value = console().readLine(label);
|
||||||
|
}
|
||||||
|
form.param(param, value);
|
||||||
|
}
|
||||||
|
response.close();
|
||||||
|
client.close();
|
||||||
|
client = createResteasyClient();
|
||||||
|
response = client.target(callback).request().post(Entity.form(form));
|
||||||
|
} else if (response.getStatus() == 302) {
|
||||||
|
int redirectCount = 0;
|
||||||
|
do {
|
||||||
|
String location = response.getLocation().toString();
|
||||||
|
Matcher m = codePattern.matcher(location);
|
||||||
|
if (!m.find()) {
|
||||||
|
response.close();
|
||||||
|
client.close();
|
||||||
|
client = createResteasyClient();
|
||||||
|
response = client.target(location).request().get();
|
||||||
|
} else {
|
||||||
|
response.close();
|
||||||
|
client.close();
|
||||||
|
String code = m.group(1);
|
||||||
|
processCode(code, redirectUri);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (response.getStatus() == 302 && redirectCount++ > 4) {
|
||||||
|
System.err.println("Too many redirects. Aborting");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} while (response.getStatus() == 302);
|
||||||
|
} else {
|
||||||
|
System.err.println("Unknown response from server: " + response.getStatus());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw ex;
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ResteasyClient getResteasyClient() {
|
||||||
|
if (this.resteasyClient == null) {
|
||||||
|
this.resteasyClient = createResteasyClient();
|
||||||
|
}
|
||||||
|
return this.resteasyClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ResteasyClient createResteasyClient() {
|
||||||
|
return new ResteasyClientBuilder()
|
||||||
|
.connectionCheckoutTimeout(1, TimeUnit.HOURS)
|
||||||
|
.connectionTTL(1, TimeUnit.HOURS)
|
||||||
|
.socketTimeout(1, TimeUnit.HOURS)
|
||||||
|
.disableTrustManager().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public String getTokenString() throws VerificationException, IOException, ServerRequest.HttpFailure {
|
public String getTokenString() throws VerificationException, IOException, ServerRequest.HttpFailure {
|
||||||
return tokenString;
|
return tokenString;
|
||||||
}
|
}
|
||||||
@@ -308,7 +525,7 @@ public class KeycloakInstalled {
|
|||||||
parseAccessToken(tokenResponse);
|
parseAccessToken(tokenResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshToken(String refreshToken) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
public void refreshToken(String refreshToken) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||||
AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken);
|
AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken);
|
||||||
parseAccessToken(tokenResponse);
|
parseAccessToken(tokenResponse);
|
||||||
|
|
||||||
@@ -360,7 +577,6 @@ public class KeycloakInstalled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void processCode(String code, String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
private void processCode(String code, String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||||
AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null);
|
AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null);
|
||||||
parseAccessToken(tokenResponse);
|
parseAccessToken(tokenResponse);
|
||||||
@@ -435,11 +651,26 @@ public class KeycloakInstalled {
|
|||||||
|
|
||||||
OutputStreamWriter out = new OutputStreamWriter(socket.getOutputStream());
|
OutputStreamWriter out = new OutputStreamWriter(socket.getOutputStream());
|
||||||
PrintWriter pw = new PrintWriter(out);
|
PrintWriter pw = new PrintWriter(out);
|
||||||
|
if (writer != null) {
|
||||||
|
System.err.println("Using a writer is deprecated. Please remove its usage. This is now handled by endpoint on server");
|
||||||
|
}
|
||||||
|
|
||||||
if (error == null) {
|
if (error == null) {
|
||||||
writer.success(pw, KeycloakInstalled.this);
|
if (writer != null) {
|
||||||
|
writer.success(pw, KeycloakInstalled.this);
|
||||||
|
} else {
|
||||||
|
pw.println("HTTP/1.1 302 Found");
|
||||||
|
pw.println("Location: " + deployment.getTokenUrl().replace("/token", "/delegated"));
|
||||||
|
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
writer.failure(pw, KeycloakInstalled.this);
|
if (writer != null) {
|
||||||
|
writer.failure(pw, KeycloakInstalled.this);
|
||||||
|
} else {
|
||||||
|
pw.println("HTTP/1.1 302 Found");
|
||||||
|
pw.println("Location: " + deployment.getTokenUrl().replace("/token", "/delegated?error=true"));
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pw.flush();
|
pw.flush();
|
||||||
socket.close();
|
socket.close();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+18
@@ -27,6 +27,9 @@ import javax.security.cert.X509Certificate;
|
|||||||
import javax.ws.rs.container.ContainerRequestContext;
|
import javax.ws.rs.container.ContainerRequestContext;
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.SecurityContext;
|
import javax.ws.rs.core.SecurityContext;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -51,6 +54,8 @@ public class JaxrsHttpFacade implements OIDCHttpFacade {
|
|||||||
|
|
||||||
protected class RequestFacade implements OIDCHttpFacade.Request {
|
protected class RequestFacade implements OIDCHttpFacade.Request {
|
||||||
|
|
||||||
|
private InputStream inputStream;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFirstParam(String param) {
|
public String getFirstParam(String param) {
|
||||||
throw new RuntimeException("NOT IMPLEMENTED");
|
throw new RuntimeException("NOT IMPLEMENTED");
|
||||||
@@ -108,6 +113,19 @@ public class JaxrsHttpFacade implements OIDCHttpFacade {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream getInputStream() {
|
public InputStream getInputStream() {
|
||||||
|
return getInputStream(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream(boolean buffered) {
|
||||||
|
if (inputStream != null) {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffered) {
|
||||||
|
return inputStream = new BufferedInputStream(requestContext.getEntityStream());
|
||||||
|
}
|
||||||
|
|
||||||
return requestContext.getEntityStream();
|
return requestContext.getEntityStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<name>Keycloak Jetty Integration</name>
|
<name>Keycloak Jetty Integration</name>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+73
-10
@@ -18,7 +18,7 @@
|
|||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import * as Keycloak from 'keycloak';
|
import * as Keycloak from './keycloak';
|
||||||
|
|
||||||
export as namespace KeycloakAuthorization;
|
export as namespace KeycloakAuthorization;
|
||||||
|
|
||||||
@@ -35,6 +35,64 @@ declare namespace KeycloakAuthorization {
|
|||||||
then(onGrant: (rpt: string) => void, onDeny: () => void, onError: () => void): void;
|
then(onGrant: (rpt: string) => void, onDeny: () => void, onError: () => void): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AuthorizationRequest {
|
||||||
|
/**
|
||||||
|
* An array of objects representing the resource and scopes.
|
||||||
|
*/
|
||||||
|
permissions?:ResourcePermission[],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A permission ticket obtained from a resource server when using UMA authorization protocol.
|
||||||
|
*/
|
||||||
|
ticket?:string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean value indicating whether the server should create permission requests to the resources
|
||||||
|
* and scopes referenced by a permission ticket. This parameter will only take effect when used together
|
||||||
|
* with the ticket parameter as part of a UMA authorization process.
|
||||||
|
*/
|
||||||
|
submitRequest?:boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines additional information about this authorization request in order to specify how it should be processed
|
||||||
|
* by the server.
|
||||||
|
*/
|
||||||
|
metadata?:AuthorizationRequestMetadata,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines whether or not this authorization request should include the current RPT. If set to true, the RPT will
|
||||||
|
* be sent and permissions in the current RPT will be included in the new RPT. Otherwise, only the permissions referenced in this
|
||||||
|
* authorization request will be granted in the new RPT.
|
||||||
|
*/
|
||||||
|
incrementalAuthorization?:boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthorizationRequestMetadata {
|
||||||
|
/**
|
||||||
|
* A boolean value indicating to the server if resource names should be included in the RPT’s permissions.
|
||||||
|
* If false, only the resource identifier is included.
|
||||||
|
*/
|
||||||
|
responseIncludeResourceName?:any,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An integer N that defines a limit for the amount of permissions an RPT can have. When used together with
|
||||||
|
* rpt parameter, only the last N requested permissions will be kept in the RPT.
|
||||||
|
*/
|
||||||
|
response_permissions_limit?:number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResourcePermission {
|
||||||
|
/**
|
||||||
|
* The id or name of a resource.
|
||||||
|
*/
|
||||||
|
id:string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of strings where each value is the name of a scope associated with the resource.
|
||||||
|
*/
|
||||||
|
scopes?:string[]
|
||||||
|
}
|
||||||
|
|
||||||
interface KeycloakAuthorizationInstance {
|
interface KeycloakAuthorizationInstance {
|
||||||
rpt: any;
|
rpt: any;
|
||||||
config: { rpt_endpoint: string };
|
config: { rpt_endpoint: string };
|
||||||
@@ -42,18 +100,23 @@ declare namespace KeycloakAuthorization {
|
|||||||
init(): void;
|
init(): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method enables client applications to better integrate with resource servers protected by a Keycloak
|
* This method enables client applications to better integrate with resource servers protected by a Keycloak
|
||||||
* policy enforcer.
|
* policy enforcer using UMA protocol.
|
||||||
*
|
*
|
||||||
* In this case, the resource server will respond with a 401 status code and a WWW-Authenticate header holding the
|
* The authorization request must be provided with a ticket.
|
||||||
* necessary information to ask a Keycloak server for authorization data using both UMA and Entitlement protocol,
|
*
|
||||||
* depending on how the policy enforcer at the resource server was configured.
|
* @param authorizationRequest An AuthorizationRequest instance with a valid permission ticket set.
|
||||||
*/
|
* @returns A promise to set functions to be invoked on grant, deny or error.
|
||||||
authorize(wwwAuthenticateHeader: string): KeycloakAuthorizationPromise;
|
*/
|
||||||
|
authorize(authorizationRequest: AuthorizationRequest): KeycloakAuthorizationPromise;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtains all entitlements from a Keycloak server based on a given resourceServerId.
|
* Obtains all entitlements from a Keycloak server based on a given resourceServerId.
|
||||||
|
*
|
||||||
|
* @param resourceServerId The id (client id) of the resource server to obtain permissions from.
|
||||||
|
* @param authorizationRequest An AuthorizationRequest instance.
|
||||||
|
* @returns A promise to set functions to be invoked on grant, deny or error.
|
||||||
*/
|
*/
|
||||||
entitlement(resourceServerId: string, entitlementRequest: {}): KeycloakAuthorizationPromise;
|
entitlement(resourceServerId: string, authorizationRequest?: AuthorizationRequest): KeycloakAuthorizationPromise;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,14 +18,14 @@
|
|||||||
|
|
||||||
(function( window, undefined ) {
|
(function( window, undefined ) {
|
||||||
|
|
||||||
var KeycloakAuthorization = function (keycloak) {
|
var KeycloakAuthorization = function (keycloak, options) {
|
||||||
var _instance = this;
|
var _instance = this;
|
||||||
this.rpt = null;
|
this.rpt = null;
|
||||||
|
|
||||||
this.init = function () {
|
this.init = function () {
|
||||||
var request = new XMLHttpRequest();
|
var request = new XMLHttpRequest();
|
||||||
|
|
||||||
request.open('GET', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/.well-known/uma-configuration');
|
request.open('GET', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/.well-known/uma2-configuration');
|
||||||
request.onreadystatechange = function () {
|
request.onreadystatechange = function () {
|
||||||
if (request.readyState == 4) {
|
if (request.readyState == 4) {
|
||||||
if (request.status == 200) {
|
if (request.status == 200) {
|
||||||
@@ -41,74 +41,65 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This method enables client applications to better integrate with resource servers protected by a Keycloak
|
* This method enables client applications to better integrate with resource servers protected by a Keycloak
|
||||||
* policy enforcer.
|
* policy enforcer using UMA protocol.
|
||||||
*
|
*
|
||||||
* In this case, the resource server will respond with a 401 status code and a WWW-Authenticate header holding the
|
* The authorization request must be provided with a ticket.
|
||||||
* necessary information to ask a Keycloak server for authorization data using both UMA and Entitlement protocol,
|
|
||||||
* depending on how the policy enforcer at the resource server was configured.
|
|
||||||
*/
|
*/
|
||||||
this.authorize = function (wwwAuthenticateHeader) {
|
this.authorize = function (authorizationRequest) {
|
||||||
this.then = function (onGrant, onDeny, onError) {
|
this.then = function (onGrant, onDeny, onError) {
|
||||||
if (wwwAuthenticateHeader.indexOf('UMA') != -1) {
|
if (authorizationRequest && authorizationRequest.ticket) {
|
||||||
var params = wwwAuthenticateHeader.split(',');
|
var request = new XMLHttpRequest();
|
||||||
|
|
||||||
for (i = 0; i < params.length; i++) {
|
request.open('POST', _instance.config.token_endpoint, true);
|
||||||
var param = params[i].split('=');
|
request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||||
|
request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token);
|
||||||
|
|
||||||
if (param[0] == 'ticket') {
|
request.onreadystatechange = function () {
|
||||||
var request = new XMLHttpRequest();
|
if (request.readyState == 4) {
|
||||||
|
var status = request.status;
|
||||||
|
|
||||||
request.open('POST', _instance.config.rpt_endpoint, true);
|
if (status >= 200 && status < 300) {
|
||||||
request.setRequestHeader('Content-Type', 'application/json')
|
var rpt = JSON.parse(request.responseText).access_token;
|
||||||
request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token)
|
_instance.rpt = rpt;
|
||||||
|
onGrant(rpt);
|
||||||
request.onreadystatechange = function () {
|
} else if (status == 403) {
|
||||||
if (request.readyState == 4) {
|
if (onDeny) {
|
||||||
var status = request.status;
|
onDeny();
|
||||||
|
} else {
|
||||||
if (status >= 200 && status < 300) {
|
console.error('Authorization request was denied by the server.');
|
||||||
var rpt = JSON.parse(request.responseText).rpt;
|
|
||||||
_instance.rpt = rpt;
|
|
||||||
onGrant(rpt);
|
|
||||||
} else if (status == 403) {
|
|
||||||
if (onDeny) {
|
|
||||||
onDeny();
|
|
||||||
} else {
|
|
||||||
console.error('Authorization request was denied by the server.');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (onError) {
|
|
||||||
onError();
|
|
||||||
} else {
|
|
||||||
console.error('Could not obtain authorization data from server.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
} else {
|
||||||
|
if (onError) {
|
||||||
var ticket = param[1].substring(1, param[1].length - 1).trim();
|
onError();
|
||||||
|
} else {
|
||||||
request.send(JSON.stringify(
|
console.error('Could not obtain authorization data from server.');
|
||||||
{
|
|
||||||
ticket: ticket,
|
|
||||||
rpt: _instance.rpt
|
|
||||||
}
|
}
|
||||||
));
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var params = "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&client_id=" + keycloak.clientId + "&ticket=" + authorizationRequest.ticket;
|
||||||
|
|
||||||
|
if (authorizationRequest.submitRequest != undefined) {
|
||||||
|
params += "&submit_request=" + authorizationRequest.submitRequest;
|
||||||
}
|
}
|
||||||
} else if (wwwAuthenticateHeader.indexOf('KC_ETT') != -1) {
|
|
||||||
var params = wwwAuthenticateHeader.substring('KC_ETT'.length).trim().split(',');
|
|
||||||
var clientId = null;
|
|
||||||
|
|
||||||
for (i = 0; i < params.length; i++) {
|
var metadata = authorizationRequest.metadata;
|
||||||
var param = params[i].split('=');
|
|
||||||
|
|
||||||
if (param[0] == 'realm') {
|
if (metadata) {
|
||||||
clientId = param[1].substring(1, param[1].length - 1).trim();
|
if (metadata.responseIncludeResourceName) {
|
||||||
|
params += "&response_include_resource_name=" + metadata.responseIncludeResourceName;
|
||||||
|
}
|
||||||
|
if (metadata.responsePermissionsLimit) {
|
||||||
|
params += "&response_permissions_limit=" + metadata.responsePermissionsLimit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_instance.entitlement(clientId).then(onGrant, onDeny, onError);
|
if (_instance.rpt && (authorizationRequest.incrementalAuthorization == undefined || authorizationRequest.incrementalAuthorization)) {
|
||||||
|
params += "&rpt=" + _instance.rpt;
|
||||||
|
}
|
||||||
|
|
||||||
|
request.send(params);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -116,20 +107,22 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtains all entitlements from a Keycloak Server based on a give resourceServerId.
|
* Obtains all entitlements from a Keycloak Server based on a given resourceServerId.
|
||||||
*/
|
*/
|
||||||
this.entitlement = function (resourceSeververId, entitlementRequest ) {
|
this.entitlement = function (resourceServerId, authorizationRequest) {
|
||||||
this.then = function (onGrant, onDeny, onError) {
|
this.then = function (onGrant, onDeny, onError) {
|
||||||
var request = new XMLHttpRequest();
|
var request = new XMLHttpRequest();
|
||||||
|
|
||||||
|
request.open('POST', _instance.config.token_endpoint, true);
|
||||||
|
request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||||
|
request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token);
|
||||||
|
|
||||||
request.onreadystatechange = function () {
|
request.onreadystatechange = function () {
|
||||||
if (request.readyState == 4) {
|
if (request.readyState == 4) {
|
||||||
var status = request.status;
|
var status = request.status;
|
||||||
|
|
||||||
if (status >= 200 && status < 300) {
|
if (status >= 200 && status < 300) {
|
||||||
var rpt = JSON.parse(request.responseText).rpt;
|
var rpt = JSON.parse(request.responseText).access_token;
|
||||||
_instance.rpt = rpt;
|
_instance.rpt = rpt;
|
||||||
onGrant(rpt);
|
onGrant(rpt);
|
||||||
} else if (status == 403) {
|
} else if (status == 403) {
|
||||||
@@ -148,25 +141,70 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var erJson = null
|
if (!authorizationRequest) {
|
||||||
|
authorizationRequest = {};
|
||||||
if(entitlementRequest) {
|
|
||||||
request.open('POST', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/authz/entitlement/' + resourceSeververId, true);
|
|
||||||
request.setRequestHeader("Content-type", "application/json");
|
|
||||||
erJson = JSON.stringify(entitlementRequest)
|
|
||||||
} else {
|
|
||||||
request.open('GET', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/authz/entitlement/' + resourceSeververId, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token)
|
var params = "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&client_id=" + keycloak.clientId;
|
||||||
request.send(erJson);
|
|
||||||
|
|
||||||
|
if (authorizationRequest.claimToken) {
|
||||||
|
params += "&claim_token=" + authorizationRequest.claimToken;
|
||||||
|
|
||||||
|
if (authorizationRequest.claimTokenFormat) {
|
||||||
|
params += "&claim_token_format=" + authorizationRequest.claimTokenFormat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params += "&audience=" + resourceServerId;
|
||||||
|
|
||||||
|
var permissions = authorizationRequest.permissions;
|
||||||
|
|
||||||
|
if (!permissions) {
|
||||||
|
permissions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < permissions.length; i++) {
|
||||||
|
var resource = permissions[i];
|
||||||
|
var permission = resource.id;
|
||||||
|
|
||||||
|
if (resource.scopes && resource.scopes.length > 0) {
|
||||||
|
permission += "#";
|
||||||
|
for (j = 0; j < resource.scopes.length; j++) {
|
||||||
|
var scope = resource.scopes[j];
|
||||||
|
if (permission.indexOf('#') != permission.length - 1) {
|
||||||
|
permission += ",";
|
||||||
|
}
|
||||||
|
permission += scope;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params += "&permission=" + permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata = authorizationRequest.metadata;
|
||||||
|
|
||||||
|
if (metadata) {
|
||||||
|
if (metadata.responseIncludeResourceName) {
|
||||||
|
params += "&response_include_resource_name=" + metadata.responseIncludeResourceName;
|
||||||
|
}
|
||||||
|
if (metadata.responsePermissionsLimit) {
|
||||||
|
params += "&response_permissions_limit=" + metadata.responsePermissionsLimit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_instance.rpt) {
|
||||||
|
params += "&rpt=" + _instance.rpt;
|
||||||
|
}
|
||||||
|
|
||||||
|
request.send(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.init(this);
|
this.init(this);
|
||||||
|
|
||||||
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
|
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
|
||||||
|
|||||||
+503
-478
@@ -1,478 +1,503 @@
|
|||||||
/*
|
/*
|
||||||
* MIT License
|
* MIT License
|
||||||
*
|
*
|
||||||
* Copyright 2017 Brett Epps <https://github.com/eppsilon>
|
* Copyright 2017 Brett Epps <https://github.com/eppsilon>
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
* associated documentation files (the "Software"), to deal in the Software without restriction, including
|
* associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
* copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
* copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
* following conditions:
|
* following conditions:
|
||||||
*
|
*
|
||||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
* portions of the Software.
|
* portions of the Software.
|
||||||
*
|
*
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||||
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
export as namespace Keycloak;
|
export as namespace Keycloak;
|
||||||
|
|
||||||
export = Keycloak;
|
export = Keycloak;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Keycloak client instance.
|
* Creates a new Keycloak client instance.
|
||||||
* @param config Path to a JSON config file or a plain config object.
|
* @param config Path to a JSON config file or a plain config object.
|
||||||
*/
|
*/
|
||||||
declare function Keycloak(config?: string|{}): Keycloak.KeycloakInstance;
|
declare function Keycloak(config?: string|{}): Keycloak.KeycloakInstance;
|
||||||
|
|
||||||
declare namespace Keycloak {
|
declare namespace Keycloak {
|
||||||
type KeycloakAdapterName = 'cordova'|'default';
|
type KeycloakAdapterName = 'cordova'|'default' | any;
|
||||||
type KeycloakOnLoad = 'login-required'|'check-sso';
|
type KeycloakOnLoad = 'login-required'|'check-sso';
|
||||||
type KeycloakResponseMode = 'query'|'fragment';
|
type KeycloakResponseMode = 'query'|'fragment';
|
||||||
type KeycloakResponseType = 'code'|'id_token token'|'code id_token token';
|
type KeycloakResponseType = 'code'|'id_token token'|'code id_token token';
|
||||||
type KeycloakFlow = 'standard'|'implicit'|'hybrid';
|
type KeycloakFlow = 'standard'|'implicit'|'hybrid';
|
||||||
|
|
||||||
interface KeycloakInitOptions {
|
interface KeycloakInitOptions {
|
||||||
/**
|
/**
|
||||||
* @private Undocumented.
|
* @private Undocumented.
|
||||||
*/
|
*/
|
||||||
adapter?: KeycloakAdapterName;
|
useNonce?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specifies an action to do on load.
|
* Allows to use different adapter:
|
||||||
*/
|
*
|
||||||
onLoad?: KeycloakOnLoad;
|
* - {string} default - using browser api for redirects
|
||||||
|
* - {string} cordova - using cordova plugins
|
||||||
/**
|
* - {function} - allows to provide custom function as adapter.
|
||||||
* Set an initial value for the token.
|
*/
|
||||||
*/
|
adapter?: KeycloakAdapterName;
|
||||||
token?: string;
|
|
||||||
|
/**
|
||||||
/**
|
* Specifies an action to do on load.
|
||||||
* Set an initial value for the refresh token.
|
*/
|
||||||
*/
|
onLoad?: KeycloakOnLoad;
|
||||||
refreshToken?: string;
|
|
||||||
|
/**
|
||||||
/**
|
* Set an initial value for the token.
|
||||||
* Set an initial value for the id token (only together with `token` or
|
*/
|
||||||
* `refreshToken`).
|
token?: string;
|
||||||
*/
|
|
||||||
idToken?: string;
|
/**
|
||||||
|
* Set an initial value for the refresh token.
|
||||||
/**
|
*/
|
||||||
* Set an initial value for skew between local time and Keycloak server in
|
refreshToken?: string;
|
||||||
* seconds (only together with `token` or `refreshToken`).
|
|
||||||
*/
|
/**
|
||||||
timeSkew?: number;
|
* Set an initial value for the id token (only together with `token` or
|
||||||
|
* `refreshToken`).
|
||||||
/**
|
*/
|
||||||
* Set to enable/disable monitoring login state.
|
idToken?: string;
|
||||||
* @default true
|
|
||||||
*/
|
/**
|
||||||
checkLoginIframe?: boolean;
|
* Set an initial value for skew between local time and Keycloak server in
|
||||||
|
* seconds (only together with `token` or `refreshToken`).
|
||||||
/**
|
*/
|
||||||
* Set the interval to check login state (in seconds).
|
timeSkew?: number;
|
||||||
* @default 5
|
|
||||||
*/
|
/**
|
||||||
checkLoginIframeInterval?: boolean;
|
* Set to enable/disable monitoring login state.
|
||||||
|
* @default true
|
||||||
/**
|
*/
|
||||||
* Set the OpenID Connect response mode to send to Keycloak upon login.
|
checkLoginIframe?: boolean;
|
||||||
* @default fragment After successful authentication Keycloak will redirect
|
|
||||||
* to JavaScript application with OpenID Connect parameters
|
/**
|
||||||
* added in URL fragment. This is generally safer and
|
* Set the interval to check login state (in seconds).
|
||||||
* recommended over query.
|
* @default 5
|
||||||
*/
|
*/
|
||||||
responseMode?: KeycloakResponseMode;
|
checkLoginIframeInterval?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the OpenID Connect flow.
|
* Set the OpenID Connect response mode to send to Keycloak upon login.
|
||||||
* @default standard
|
* @default fragment After successful authentication Keycloak will redirect
|
||||||
*/
|
* to JavaScript application with OpenID Connect parameters
|
||||||
flow?: KeycloakFlow;
|
* added in URL fragment. This is generally safer and
|
||||||
}
|
* recommended over query.
|
||||||
|
*/
|
||||||
interface KeycloakLoginOptions {
|
responseMode?: KeycloakResponseMode;
|
||||||
/**
|
|
||||||
* @private Undocumented.
|
/**
|
||||||
*/
|
* Set the OpenID Connect flow.
|
||||||
scope?: string;
|
* @default standard
|
||||||
|
*/
|
||||||
/**
|
flow?: KeycloakFlow;
|
||||||
* Specifies the uri to redirect to after login.
|
}
|
||||||
*/
|
|
||||||
redirectUri?: string;
|
interface KeycloakLoginOptions {
|
||||||
|
/**
|
||||||
/**
|
* @private Undocumented.
|
||||||
* By default the login screen is displayed if the user is not logged into
|
*/
|
||||||
* Keycloak. To only authenticate to the application if the user is already
|
scope?: string;
|
||||||
* logged in and not display the login page if the user is not logged in, set
|
|
||||||
* this option to `'none'`. To always require re-authentication and ignore
|
/**
|
||||||
* SSO, set this option to `'login'`.
|
* Specifies the uri to redirect to after login.
|
||||||
*/
|
*/
|
||||||
prompt?: 'none'|'login';
|
redirectUri?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If value is `'register'` then user is redirected to registration page,
|
* By default the login screen is displayed if the user is not logged into
|
||||||
* otherwise to login page.
|
* Keycloak. To only authenticate to the application if the user is already
|
||||||
*/
|
* logged in and not display the login page if the user is not logged in, set
|
||||||
action?: 'register';
|
* this option to `'none'`. To always require re-authentication and ignore
|
||||||
|
* SSO, set this option to `'login'`.
|
||||||
/**
|
*/
|
||||||
* Used just if user is already authenticated. Specifies maximum time since
|
prompt?: 'none'|'login';
|
||||||
* the authentication of user happened. If user is already authenticated for
|
|
||||||
* longer time than `'maxAge'`, the SSO is ignored and he will need to
|
/**
|
||||||
* authenticate again.
|
* If value is `'register'` then user is redirected to registration page,
|
||||||
*/
|
* otherwise to login page.
|
||||||
maxAge?: number;
|
*/
|
||||||
|
action?: 'register';
|
||||||
/**
|
|
||||||
* Used to pre-fill the username/email field on the login form.
|
/**
|
||||||
*/
|
* Used just if user is already authenticated. Specifies maximum time since
|
||||||
loginHint?: string;
|
* the authentication of user happened. If user is already authenticated for
|
||||||
|
* longer time than `'maxAge'`, the SSO is ignored and he will need to
|
||||||
/**
|
* authenticate again.
|
||||||
* Used to tell Keycloak which IDP the user wants to authenticate with.
|
*/
|
||||||
*/
|
maxAge?: number;
|
||||||
idpHint?: string;
|
|
||||||
|
/**
|
||||||
/**
|
* Used to pre-fill the username/email field on the login form.
|
||||||
* Specifies the desired locale for the UI.
|
*/
|
||||||
*/
|
loginHint?: string;
|
||||||
locale?: string;
|
|
||||||
}
|
/**
|
||||||
|
* Used to tell Keycloak which IDP the user wants to authenticate with.
|
||||||
type KeycloakPromiseCallback<T> = (result: T) => void;
|
*/
|
||||||
|
idpHint?: string;
|
||||||
interface KeycloakPromise<TSuccess, TError> {
|
|
||||||
/**
|
/**
|
||||||
* Function to call if the promised action succeeds.
|
* Sets the 'ui_locales' query param in compliance with section 3.1.2.1
|
||||||
*/
|
* of the OIDC 1.0 specification.
|
||||||
success(callback: KeycloakPromiseCallback<TSuccess>): KeycloakPromise<TSuccess, TError>;
|
*/
|
||||||
|
locale?: string;
|
||||||
/**
|
|
||||||
* Function to call if the promised action throws an error.
|
/**
|
||||||
*/
|
* Specifies the desired Keycloak locale for the UI. This differs from
|
||||||
error(callback: KeycloakPromiseCallback<TError>): KeycloakPromise<TSuccess, TError>;
|
* the locale param in that it tells the Keycloak server to set a cookie and update
|
||||||
}
|
* the user's profile to a new preferred locale.
|
||||||
|
*/
|
||||||
interface KeycloakError {
|
kcLocale?: string;
|
||||||
error: string;
|
|
||||||
error_description: string;
|
/**
|
||||||
}
|
* Specifies arguments that are passed to the Cordova in-app-browser (if applicable).
|
||||||
|
* Options 'hidden' and 'location' are not affected by these arguments.
|
||||||
interface KeycloakAdapter {
|
* All available options are defined at https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-inappbrowser/.
|
||||||
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
* Example of use: { zoom: "no", hardwareback: "yes" }
|
||||||
logout(options?: any): KeycloakPromise<void, void>;
|
*/
|
||||||
register(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
cordovaOptions?: { [optionName: string]: string };
|
||||||
accountManagement(): KeycloakPromise<void, void>;
|
}
|
||||||
redirectUri(options: { redirectUri: string; }, encodeHash: boolean): string;
|
|
||||||
}
|
type KeycloakPromiseCallback<T> = (result: T) => void;
|
||||||
|
|
||||||
interface KeycloakProfile {
|
interface KeycloakPromise<TSuccess, TError> {
|
||||||
id?: string;
|
/**
|
||||||
username?: string;
|
* Function to call if the promised action succeeds.
|
||||||
email?: string;
|
*/
|
||||||
firstName?: string;
|
success(callback: KeycloakPromiseCallback<TSuccess>): KeycloakPromise<TSuccess, TError>;
|
||||||
lastName?: string;
|
|
||||||
enabled?: boolean;
|
/**
|
||||||
emailVerified?: boolean;
|
* Function to call if the promised action throws an error.
|
||||||
totp?: boolean;
|
*/
|
||||||
createdTimestamp?: number;
|
error(callback: KeycloakPromiseCallback<TError>): KeycloakPromise<TSuccess, TError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// export interface KeycloakUserInfo {}
|
interface KeycloakError {
|
||||||
|
error: string;
|
||||||
/**
|
error_description: string;
|
||||||
* A client for the Keycloak authentication server.
|
}
|
||||||
* @see {@link https://keycloak.gitbooks.io/securing-client-applications-guide/content/topics/oidc/javascript-adapter.html|Keycloak JS adapter documentation}
|
|
||||||
*/
|
interface KeycloakAdapter {
|
||||||
interface KeycloakInstance {
|
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||||
/**
|
logout(options?: any): KeycloakPromise<void, void>;
|
||||||
* Is true if the user is authenticated, false otherwise.
|
register(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||||
*/
|
accountManagement(): KeycloakPromise<void, void>;
|
||||||
authenticated?: boolean;
|
redirectUri(options: { redirectUri: string; }, encodeHash: boolean): string;
|
||||||
|
}
|
||||||
/**
|
|
||||||
* The user id.
|
interface KeycloakProfile {
|
||||||
*/
|
id?: string;
|
||||||
subject?: string;
|
username?: string;
|
||||||
|
email?: string;
|
||||||
/**
|
firstName?: string;
|
||||||
* Response mode passed in init (default value is `'fragment'`).
|
lastName?: string;
|
||||||
*/
|
enabled?: boolean;
|
||||||
responseMode?: KeycloakResponseMode;
|
emailVerified?: boolean;
|
||||||
|
totp?: boolean;
|
||||||
/**
|
createdTimestamp?: number;
|
||||||
* Response type sent to Keycloak with login requests. This is determined
|
}
|
||||||
* based on the flow value used during initialization, but can be overridden
|
|
||||||
* by setting this value.
|
// export interface KeycloakUserInfo {}
|
||||||
*/
|
|
||||||
responseType?: KeycloakResponseType;
|
/**
|
||||||
|
* A client for the Keycloak authentication server.
|
||||||
/**
|
* @see {@link https://keycloak.gitbooks.io/securing-client-applications-guide/content/topics/oidc/javascript-adapter.html|Keycloak JS adapter documentation}
|
||||||
* Flow passed in init.
|
*/
|
||||||
*/
|
interface KeycloakInstance {
|
||||||
flow?: KeycloakFlow;
|
/**
|
||||||
|
* Is true if the user is authenticated, false otherwise.
|
||||||
/**
|
*/
|
||||||
* The realm roles associated with the token.
|
authenticated?: boolean;
|
||||||
*/
|
|
||||||
realmAccess?: { roles: string[] };
|
/**
|
||||||
|
* The user id.
|
||||||
/**
|
*/
|
||||||
* The resource roles associated with the token.
|
subject?: string;
|
||||||
*/
|
|
||||||
resourceAccess?: string[];
|
/**
|
||||||
|
* Response mode passed in init (default value is `'fragment'`).
|
||||||
/**
|
*/
|
||||||
* The base64 encoded token that can be sent in the Authorization header in
|
responseMode?: KeycloakResponseMode;
|
||||||
* requests to services.
|
|
||||||
*/
|
/**
|
||||||
token?: string;
|
* Response type sent to Keycloak with login requests. This is determined
|
||||||
|
* based on the flow value used during initialization, but can be overridden
|
||||||
/**
|
* by setting this value.
|
||||||
* The parsed token as a JavaScript object.
|
*/
|
||||||
*/
|
responseType?: KeycloakResponseType;
|
||||||
tokenParsed?: {
|
|
||||||
exp?: number;
|
/**
|
||||||
iat?: number;
|
* Flow passed in init.
|
||||||
nonce?: string;
|
*/
|
||||||
sub?: string;
|
flow?: KeycloakFlow;
|
||||||
session_state?: string;
|
|
||||||
realm_access?: { roles: string[] };
|
/**
|
||||||
resource_access?: string[];
|
* The realm roles associated with the token.
|
||||||
};
|
*/
|
||||||
|
realmAccess?: { roles: string[] };
|
||||||
/**
|
|
||||||
* The base64 encoded refresh token that can be used to retrieve a new token.
|
/**
|
||||||
*/
|
* The resource roles associated with the token.
|
||||||
refreshToken?: string;
|
*/
|
||||||
|
resourceAccess?: string[];
|
||||||
/**
|
|
||||||
* The parsed refresh token as a JavaScript object.
|
/**
|
||||||
*/
|
* The base64 encoded token that can be sent in the Authorization header in
|
||||||
refreshTokenParsed?: { nonce?: string };
|
* requests to services.
|
||||||
|
*/
|
||||||
/**
|
token?: string;
|
||||||
* The base64 encoded ID token.
|
|
||||||
*/
|
/**
|
||||||
idToken?: string;
|
* The parsed token as a JavaScript object.
|
||||||
|
*/
|
||||||
/**
|
tokenParsed?: {
|
||||||
* The parsed id token as a JavaScript object.
|
exp?: number;
|
||||||
*/
|
iat?: number;
|
||||||
idTokenParsed?: { nonce?: string };
|
nonce?: string;
|
||||||
|
sub?: string;
|
||||||
/**
|
session_state?: string;
|
||||||
* The estimated time difference between the browser time and the Keycloak
|
realm_access?: { roles: string[] };
|
||||||
* server in seconds. This value is just an estimation, but is accurate
|
resource_access?: string[];
|
||||||
* enough when determining if a token is expired or not.
|
};
|
||||||
*/
|
|
||||||
timeSkew?: number;
|
/**
|
||||||
|
* The base64 encoded refresh token that can be used to retrieve a new token.
|
||||||
/**
|
*/
|
||||||
* @private Undocumented.
|
refreshToken?: string;
|
||||||
*/
|
|
||||||
loginRequired?: boolean;
|
/**
|
||||||
|
* The parsed refresh token as a JavaScript object.
|
||||||
/**
|
*/
|
||||||
* @private Undocumented.
|
refreshTokenParsed?: { nonce?: string };
|
||||||
*/
|
|
||||||
authServerUrl?: string;
|
/**
|
||||||
|
* The base64 encoded ID token.
|
||||||
/**
|
*/
|
||||||
* @private Undocumented.
|
idToken?: string;
|
||||||
*/
|
|
||||||
realm?: string;
|
/**
|
||||||
|
* The parsed id token as a JavaScript object.
|
||||||
/**
|
*/
|
||||||
* @private Undocumented.
|
idTokenParsed?: { nonce?: string };
|
||||||
*/
|
|
||||||
clientId?: string;
|
/**
|
||||||
|
* The estimated time difference between the browser time and the Keycloak
|
||||||
/**
|
* server in seconds. This value is just an estimation, but is accurate
|
||||||
* @private Undocumented.
|
* enough when determining if a token is expired or not.
|
||||||
*/
|
*/
|
||||||
clientSecret?: string;
|
timeSkew?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private Undocumented.
|
* @private Undocumented.
|
||||||
*/
|
*/
|
||||||
redirectUri?: string;
|
loginRequired?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private Undocumented.
|
* @private Undocumented.
|
||||||
*/
|
*/
|
||||||
sessionId?: string;
|
authServerUrl?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private Undocumented.
|
* @private Undocumented.
|
||||||
*/
|
*/
|
||||||
profile?: KeycloakProfile;
|
realm?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private Undocumented.
|
* @private Undocumented.
|
||||||
*/
|
*/
|
||||||
userInfo?: {}; // KeycloakUserInfo;
|
clientId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the adapter is initialized.
|
* @private Undocumented.
|
||||||
*/
|
*/
|
||||||
onReady?(authenticated?: boolean): void;
|
clientSecret?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a user is successfully authenticated.
|
* @private Undocumented.
|
||||||
*/
|
*/
|
||||||
onAuthSuccess?(): void;
|
redirectUri?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called if there was an error during authentication.
|
* @private Undocumented.
|
||||||
*/
|
*/
|
||||||
onAuthError?(errorData: KeycloakError): void;
|
sessionId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the token is refreshed.
|
* @private Undocumented.
|
||||||
*/
|
*/
|
||||||
onAuthRefreshSuccess?(): void;
|
profile?: KeycloakProfile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called if there was an error while trying to refresh the token.
|
* @private Undocumented.
|
||||||
*/
|
*/
|
||||||
onAuthRefreshError?(): void;
|
userInfo?: {}; // KeycloakUserInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called if the user is logged out (will only be called if the session
|
* Called when the adapter is initialized.
|
||||||
* status iframe is enabled, or in Cordova mode).
|
*/
|
||||||
*/
|
onReady?(authenticated?: boolean): void;
|
||||||
onAuthLogout?(): void;
|
|
||||||
|
/**
|
||||||
/**
|
* Called when a user is successfully authenticated.
|
||||||
* Called when the access token is expired. If a refresh token is available
|
*/
|
||||||
* the token can be refreshed with Keycloak#updateToken, or in cases where
|
onAuthSuccess?(): void;
|
||||||
* it's not (ie. with implicit flow) you can redirect to login screen to
|
|
||||||
* obtain a new access token.
|
/**
|
||||||
*/
|
* Called if there was an error during authentication.
|
||||||
onTokenExpired?(): void;
|
*/
|
||||||
|
onAuthError?(errorData: KeycloakError): void;
|
||||||
/**
|
|
||||||
* Called to initialize the adapter.
|
/**
|
||||||
* @param initOptions Initialization options.
|
* Called when the token is refreshed.
|
||||||
* @returns A promise to set functions to be invoked on success or error.
|
*/
|
||||||
*/
|
onAuthRefreshSuccess?(): void;
|
||||||
init(initOptions: KeycloakInitOptions): KeycloakPromise<boolean, KeycloakError>;
|
|
||||||
|
/**
|
||||||
/**
|
* Called if there was an error while trying to refresh the token.
|
||||||
* Redirects to login form.
|
*/
|
||||||
* @param options Login options.
|
onAuthRefreshError?(): void;
|
||||||
*/
|
|
||||||
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
/**
|
||||||
|
* Called if the user is logged out (will only be called if the session
|
||||||
/**
|
* status iframe is enabled, or in Cordova mode).
|
||||||
* Redirects to logout.
|
*/
|
||||||
* @param options Logout options.
|
onAuthLogout?(): void;
|
||||||
* @param options.redirectUri Specifies the uri to redirect to after logout.
|
|
||||||
*/
|
/**
|
||||||
logout(options?: any): KeycloakPromise<void, void>;
|
* Called when the access token is expired. If a refresh token is available
|
||||||
|
* the token can be refreshed with Keycloak#updateToken, or in cases where
|
||||||
/**
|
* it's not (ie. with implicit flow) you can redirect to login screen to
|
||||||
* Redirects to registration form.
|
* obtain a new access token.
|
||||||
* @param options Supports same options as Keycloak#login but `action` is
|
*/
|
||||||
* set to `'register'`.
|
onTokenExpired?(): void;
|
||||||
*/
|
|
||||||
register(options?: any): KeycloakPromise<void, void>;
|
/**
|
||||||
|
* Called to initialize the adapter.
|
||||||
/**
|
* @param initOptions Initialization options.
|
||||||
* Redirects to the Account Management Console.
|
* @returns A promise to set functions to be invoked on success or error.
|
||||||
*/
|
*/
|
||||||
accountManagement(): KeycloakPromise<void, void>;
|
init(initOptions: KeycloakInitOptions): KeycloakPromise<boolean, KeycloakError>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the URL to login form.
|
* Redirects to login form.
|
||||||
* @param options Supports same options as Keycloak#login.
|
* @param options Login options.
|
||||||
*/
|
*/
|
||||||
createLoginUrl(options?: KeycloakLoginOptions): string;
|
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the URL to logout the user.
|
* Redirects to logout.
|
||||||
* @param options Logout options.
|
* @param options Logout options.
|
||||||
* @param options.redirectUri Specifies the uri to redirect to after logout.
|
* @param options.redirectUri Specifies the uri to redirect to after logout.
|
||||||
*/
|
*/
|
||||||
createLogoutUrl(options?: any): string;
|
logout(options?: any): KeycloakPromise<void, void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the URL to registration page.
|
* Redirects to registration form.
|
||||||
* @param options Supports same options as Keycloak#createLoginUrl but
|
* @param options Supports same options as Keycloak#login but `action` is
|
||||||
* `action` is set to `'register'`.
|
* set to `'register'`.
|
||||||
*/
|
*/
|
||||||
createRegisterUrl(options?: KeycloakLoginOptions): string;
|
register(options?: any): KeycloakPromise<void, void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the URL to the Account Management Console.
|
* Redirects to the Account Management Console.
|
||||||
*/
|
*/
|
||||||
createAccountUrl(): string;
|
accountManagement(): KeycloakPromise<void, void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the token has less than `minValidity` seconds left before
|
* Returns the URL to login form.
|
||||||
* it expires.
|
* @param options Supports same options as Keycloak#login.
|
||||||
* @param minValidity If not specified, `0` is used.
|
*/
|
||||||
*/
|
createLoginUrl(options?: KeycloakLoginOptions): string;
|
||||||
isTokenExpired(minValidity?: number): boolean;
|
|
||||||
|
/**
|
||||||
/**
|
* Returns the URL to logout the user.
|
||||||
* If the token expires within `minValidity` seconds, the token is refreshed.
|
* @param options Logout options.
|
||||||
* If the session status iframe is enabled, the session status is also
|
* @param options.redirectUri Specifies the uri to redirect to after logout.
|
||||||
* checked.
|
*/
|
||||||
* @returns A promise to set functions that can be invoked if the token is
|
createLogoutUrl(options?: any): string;
|
||||||
* still valid, or if the token is no longer valid.
|
|
||||||
* @example
|
/**
|
||||||
* ```js
|
* Returns the URL to registration page.
|
||||||
* keycloak.updateToken(5).success(function(refreshed) {
|
* @param options Supports same options as Keycloak#createLoginUrl but
|
||||||
* if (refreshed) {
|
* `action` is set to `'register'`.
|
||||||
* alert('Token was successfully refreshed');
|
*/
|
||||||
* } else {
|
createRegisterUrl(options?: KeycloakLoginOptions): string;
|
||||||
* alert('Token is still valid');
|
|
||||||
* }
|
/**
|
||||||
* }).error(function() {
|
* Returns the URL to the Account Management Console.
|
||||||
* alert('Failed to refresh the token, or the session has expired');
|
*/
|
||||||
* });
|
createAccountUrl(): string;
|
||||||
*/
|
|
||||||
updateToken(minValidity: number): KeycloakPromise<boolean, boolean>;
|
/**
|
||||||
|
* Returns true if the token has less than `minValidity` seconds left before
|
||||||
/**
|
* it expires.
|
||||||
* Clears authentication state, including tokens. This can be useful if
|
* @param minValidity If not specified, `0` is used.
|
||||||
* the application has detected the session was expired, for example if
|
*/
|
||||||
* updating token fails. Invoking this results in Keycloak#onAuthLogout
|
isTokenExpired(minValidity?: number): boolean;
|
||||||
* callback listener being invoked.
|
|
||||||
*/
|
/**
|
||||||
clearToken(): void;
|
* If the token expires within `minValidity` seconds, the token is refreshed.
|
||||||
|
* If the session status iframe is enabled, the session status is also
|
||||||
/**
|
* checked.
|
||||||
* Returns true if the token has the given realm role.
|
* @returns A promise to set functions that can be invoked if the token is
|
||||||
* @param role A realm role name.
|
* still valid, or if the token is no longer valid.
|
||||||
*/
|
* @example
|
||||||
hasRealmRole(role: string): boolean;
|
* ```js
|
||||||
|
* keycloak.updateToken(5).success(function(refreshed) {
|
||||||
/**
|
* if (refreshed) {
|
||||||
* Returns true if the token has the given role for the resource.
|
* alert('Token was successfully refreshed');
|
||||||
* @param role A role name.
|
* } else {
|
||||||
* @param resource If not specified, `clientId` is used.
|
* alert('Token is still valid');
|
||||||
*/
|
* }
|
||||||
hasResourceRole(role: string, resource?: string): boolean;
|
* }).error(function() {
|
||||||
|
* alert('Failed to refresh the token, or the session has expired');
|
||||||
/**
|
* });
|
||||||
* Loads the user's profile.
|
*/
|
||||||
* @returns A promise to set functions to be invoked on success or error.
|
updateToken(minValidity: number): KeycloakPromise<boolean, boolean>;
|
||||||
*/
|
|
||||||
loadUserProfile(): KeycloakPromise<KeycloakProfile, void>;
|
/**
|
||||||
|
* Clears authentication state, including tokens. This can be useful if
|
||||||
/**
|
* the application has detected the session was expired, for example if
|
||||||
* @private Undocumented.
|
* updating token fails. Invoking this results in Keycloak#onAuthLogout
|
||||||
*/
|
* callback listener being invoked.
|
||||||
loadUserInfo(): KeycloakPromise<{}, void>;
|
*/
|
||||||
}
|
clearToken(): void;
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Returns true if the token has the given realm role.
|
||||||
|
* @param role A realm role name.
|
||||||
|
*/
|
||||||
|
hasRealmRole(role: string): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the token has the given role for the resource.
|
||||||
|
* @param role A role name.
|
||||||
|
* @param resource If not specified, `clientId` is used.
|
||||||
|
*/
|
||||||
|
hasResourceRole(role: string, resource?: string): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the user's profile.
|
||||||
|
* @returns A promise to set functions to be invoked on success or error.
|
||||||
|
*/
|
||||||
|
loadUserProfile(): KeycloakPromise<KeycloakProfile, void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private Undocumented.
|
||||||
|
*/
|
||||||
|
loadUserInfo(): KeycloakPromise<{}, void>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var useNonce = true;
|
||||||
|
|
||||||
kc.init = function (initOptions) {
|
kc.init = function (initOptions) {
|
||||||
kc.authenticated = false;
|
kc.authenticated = false;
|
||||||
|
|
||||||
@@ -49,6 +51,8 @@
|
|||||||
adapter = loadAdapter('cordova');
|
adapter = loadAdapter('cordova');
|
||||||
} else if (initOptions && initOptions.adapter === 'default') {
|
} else if (initOptions && initOptions.adapter === 'default') {
|
||||||
adapter = loadAdapter();
|
adapter = loadAdapter();
|
||||||
|
} else if (initOptions && typeof initOptions.adapter === "object") {
|
||||||
|
adapter = initOptions.adapter;
|
||||||
} else {
|
} else {
|
||||||
if (window.Cordova || window.cordova) {
|
if (window.Cordova || window.cordova) {
|
||||||
adapter = loadAdapter('cordova');
|
adapter = loadAdapter('cordova');
|
||||||
@@ -58,6 +62,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (initOptions) {
|
if (initOptions) {
|
||||||
|
if (typeof initOptions.useNonce !== 'undefined') {
|
||||||
|
useNonce = initOptions.useNonce;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof initOptions.checkLoginIframe !== 'undefined') {
|
if (typeof initOptions.checkLoginIframe !== 'undefined') {
|
||||||
loginIframe.enable = initOptions.checkLoginIframe;
|
loginIframe.enable = initOptions.checkLoginIframe;
|
||||||
}
|
}
|
||||||
@@ -159,8 +167,11 @@
|
|||||||
var callback = parseCallback(window.location.href);
|
var callback = parseCallback(window.location.href);
|
||||||
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
|
window.history.replaceState({}, null, callback.newUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback && callback.valid) {
|
||||||
return setupCheckLoginIframe().success(function() {
|
return setupCheckLoginIframe().success(function() {
|
||||||
window.history.replaceState({}, null, callback.newUrl);
|
|
||||||
processCallback(callback, initPromise);
|
processCallback(callback, initPromise);
|
||||||
}).error(function (e) {
|
}).error(function (e) {
|
||||||
initPromise.setError();
|
initPromise.setError();
|
||||||
@@ -232,22 +243,34 @@
|
|||||||
|
|
||||||
callbackStorage.add(callbackState);
|
callbackStorage.add(callbackState);
|
||||||
|
|
||||||
var action = 'auth';
|
var baseUrl;
|
||||||
if (options && options.action == 'register') {
|
if (options && options.action == 'register') {
|
||||||
action = 'registrations';
|
baseUrl = kc.endpoints.register();
|
||||||
|
} else {
|
||||||
|
baseUrl = kc.endpoints.authorize();
|
||||||
}
|
}
|
||||||
|
|
||||||
var scope = (options && options.scope) ? "openid " + options.scope : "openid";
|
var scope;
|
||||||
|
if (options && options.scope) {
|
||||||
|
if (options.scope.indexOf("openid") != -1) {
|
||||||
|
scope = options.scope;
|
||||||
|
} else {
|
||||||
|
scope = "openid " + options.scope;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scope = "openid";
|
||||||
|
}
|
||||||
|
|
||||||
var url = getRealmUrl()
|
var url = baseUrl
|
||||||
+ '/protocol/openid-connect/' + action
|
|
||||||
+ '?client_id=' + encodeURIComponent(kc.clientId)
|
+ '?client_id=' + encodeURIComponent(kc.clientId)
|
||||||
+ '&redirect_uri=' + encodeURIComponent(redirectUri)
|
+ '&redirect_uri=' + encodeURIComponent(redirectUri)
|
||||||
+ '&state=' + encodeURIComponent(state)
|
+ '&state=' + encodeURIComponent(state)
|
||||||
+ '&nonce=' + encodeURIComponent(nonce)
|
|
||||||
+ '&response_mode=' + encodeURIComponent(kc.responseMode)
|
+ '&response_mode=' + encodeURIComponent(kc.responseMode)
|
||||||
+ '&response_type=' + encodeURIComponent(kc.responseType)
|
+ '&response_type=' + encodeURIComponent(kc.responseType)
|
||||||
+ '&scope=' + encodeURIComponent(scope);
|
+ '&scope=' + encodeURIComponent(scope);
|
||||||
|
if (useNonce) {
|
||||||
|
url = url + '&nonce=' + encodeURIComponent(nonce);
|
||||||
|
}
|
||||||
|
|
||||||
if (options && options.prompt) {
|
if (options && options.prompt) {
|
||||||
url += '&prompt=' + encodeURIComponent(options.prompt);
|
url += '&prompt=' + encodeURIComponent(options.prompt);
|
||||||
@@ -268,6 +291,10 @@
|
|||||||
if (options && options.locale) {
|
if (options && options.locale) {
|
||||||
url += '&ui_locales=' + encodeURIComponent(options.locale);
|
url += '&ui_locales=' + encodeURIComponent(options.locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options && options.kcLocale) {
|
||||||
|
url += '&kc_locale=' + encodeURIComponent(options.kcLocale);
|
||||||
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
@@ -277,8 +304,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
kc.createLogoutUrl = function(options) {
|
kc.createLogoutUrl = function(options) {
|
||||||
var url = getRealmUrl()
|
var url = kc.endpoints.logout()
|
||||||
+ '/protocol/openid-connect/logout'
|
|
||||||
+ '?redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false));
|
+ '?redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false));
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
@@ -297,11 +323,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
kc.createAccountUrl = function(options) {
|
kc.createAccountUrl = function(options) {
|
||||||
var url = getRealmUrl()
|
var realm = getRealmUrl();
|
||||||
|
var url = undefined;
|
||||||
|
if (typeof realm !== 'undefined') {
|
||||||
|
url = realm
|
||||||
+ '/account'
|
+ '/account'
|
||||||
+ '?referrer=' + encodeURIComponent(kc.clientId)
|
+ '?referrer=' + encodeURIComponent(kc.clientId)
|
||||||
+ '&referrer_uri=' + encodeURIComponent(adapter.redirectUri(options));
|
+ '&referrer_uri=' + encodeURIComponent(adapter.redirectUri(options));
|
||||||
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +378,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
kc.loadUserInfo = function() {
|
kc.loadUserInfo = function() {
|
||||||
var url = getRealmUrl() + '/protocol/openid-connect/userinfo';
|
var url = kc.endpoints.userinfo();
|
||||||
var req = new XMLHttpRequest();
|
var req = new XMLHttpRequest();
|
||||||
req.open('GET', url, true);
|
req.open('GET', url, true);
|
||||||
req.setRequestHeader('Accept', 'application/json');
|
req.setRequestHeader('Accept', 'application/json');
|
||||||
@@ -414,7 +443,7 @@
|
|||||||
promise.setSuccess(false);
|
promise.setSuccess(false);
|
||||||
} else {
|
} else {
|
||||||
var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken;
|
var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken;
|
||||||
var url = getRealmUrl() + '/protocol/openid-connect/token';
|
var url = kc.endpoints.token();
|
||||||
|
|
||||||
refreshQueue.push(promise);
|
refreshQueue.push(promise);
|
||||||
|
|
||||||
@@ -450,6 +479,10 @@
|
|||||||
} else {
|
} else {
|
||||||
console.warn('[KEYCLOAK] Failed to refresh token');
|
console.warn('[KEYCLOAK] Failed to refresh token');
|
||||||
|
|
||||||
|
if (req.status == 400) {
|
||||||
|
kc.clearToken();
|
||||||
|
}
|
||||||
|
|
||||||
kc.onAuthRefreshError && kc.onAuthRefreshError();
|
kc.onAuthRefreshError && kc.onAuthRefreshError();
|
||||||
for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) {
|
for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) {
|
||||||
p.setError(true);
|
p.setError(true);
|
||||||
@@ -488,10 +521,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getRealmUrl() {
|
function getRealmUrl() {
|
||||||
if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') {
|
if (typeof kc.authServerUrl !== 'undefined') {
|
||||||
return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm);
|
if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') {
|
||||||
|
return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm);
|
||||||
|
} else {
|
||||||
|
return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm);
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,7 +562,7 @@
|
|||||||
|
|
||||||
if ((kc.flow != 'implicit') && code) {
|
if ((kc.flow != 'implicit') && code) {
|
||||||
var params = 'code=' + code + '&grant_type=authorization_code';
|
var params = 'code=' + code + '&grant_type=authorization_code';
|
||||||
var url = getRealmUrl() + '/protocol/openid-connect/token';
|
var url = kc.endpoints.token();
|
||||||
|
|
||||||
var req = new XMLHttpRequest();
|
var req = new XMLHttpRequest();
|
||||||
req.open('POST', url, true);
|
req.open('POST', url, true);
|
||||||
@@ -562,9 +599,9 @@
|
|||||||
|
|
||||||
setToken(accessToken, refreshToken, idToken, timeLocal);
|
setToken(accessToken, refreshToken, idToken, timeLocal);
|
||||||
|
|
||||||
if ((kc.tokenParsed && kc.tokenParsed.nonce != oauth.storedNonce) ||
|
if (useNonce && ((kc.tokenParsed && kc.tokenParsed.nonce != oauth.storedNonce) ||
|
||||||
(kc.refreshTokenParsed && kc.refreshTokenParsed.nonce != oauth.storedNonce) ||
|
(kc.refreshTokenParsed && kc.refreshTokenParsed.nonce != oauth.storedNonce) ||
|
||||||
(kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce)) {
|
(kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce))) {
|
||||||
|
|
||||||
console.info('[KEYCLOAK] Invalid nonce, clearing token');
|
console.info('[KEYCLOAK] Invalid nonce, clearing token');
|
||||||
kc.clearToken();
|
kc.clearToken();
|
||||||
@@ -589,6 +626,65 @@
|
|||||||
configUrl = config;
|
configUrl = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupOidcEndoints(oidcConfiguration) {
|
||||||
|
if (! oidcConfiguration) {
|
||||||
|
kc.endpoints = {
|
||||||
|
authorize: function() {
|
||||||
|
return getRealmUrl() + '/protocol/openid-connect/auth';
|
||||||
|
},
|
||||||
|
token: function() {
|
||||||
|
return getRealmUrl() + '/protocol/openid-connect/token';
|
||||||
|
},
|
||||||
|
logout: function() {
|
||||||
|
return getRealmUrl() + '/protocol/openid-connect/logout';
|
||||||
|
},
|
||||||
|
checkSessionIframe: function() {
|
||||||
|
var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html';
|
||||||
|
if (kc.iframeVersion) {
|
||||||
|
src = src + '?version=' + kc.iframeVersion;
|
||||||
|
}
|
||||||
|
return src;
|
||||||
|
},
|
||||||
|
register: function() {
|
||||||
|
return getRealmUrl() + '/protocol/openid-connect/registrations';
|
||||||
|
},
|
||||||
|
userinfo: function() {
|
||||||
|
return getRealmUrl() + '/protocol/openid-connect/userinfo';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
kc.endpoints = {
|
||||||
|
authorize: function() {
|
||||||
|
return oidcConfiguration.authorization_endpoint;
|
||||||
|
},
|
||||||
|
token: function() {
|
||||||
|
return oidcConfiguration.token_endpoint;
|
||||||
|
},
|
||||||
|
logout: function() {
|
||||||
|
if (!oidcConfiguration.end_session_endpoint) {
|
||||||
|
throw "Not supported by the OIDC server";
|
||||||
|
}
|
||||||
|
return oidcConfiguration.end_session_endpoint;
|
||||||
|
},
|
||||||
|
checkSessionIframe: function() {
|
||||||
|
if (!oidcConfiguration.check_session_iframe) {
|
||||||
|
throw "Not supported by the OIDC server";
|
||||||
|
}
|
||||||
|
return oidcConfiguration.check_session_iframe;
|
||||||
|
},
|
||||||
|
register: function() {
|
||||||
|
throw 'Redirection to "Register user" page not supported in standard OIDC mode';
|
||||||
|
},
|
||||||
|
userinfo: function() {
|
||||||
|
if (!oidcConfiguration.userinfo_endpoint) {
|
||||||
|
throw "Not supported by the OIDC server";
|
||||||
|
}
|
||||||
|
return oidcConfiguration.userinfo_endpoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (configUrl) {
|
if (configUrl) {
|
||||||
var req = new XMLHttpRequest();
|
var req = new XMLHttpRequest();
|
||||||
req.open('GET', configUrl, true);
|
req.open('GET', configUrl, true);
|
||||||
@@ -603,7 +699,7 @@
|
|||||||
kc.realm = config['realm'];
|
kc.realm = config['realm'];
|
||||||
kc.clientId = config['resource'];
|
kc.clientId = config['resource'];
|
||||||
kc.clientSecret = (config['credentials'] || {})['secret'];
|
kc.clientSecret = (config['credentials'] || {})['secret'];
|
||||||
|
setupOidcEndoints(null);
|
||||||
promise.setSuccess();
|
promise.setSuccess();
|
||||||
} else {
|
} else {
|
||||||
promise.setError();
|
promise.setError();
|
||||||
@@ -613,30 +709,62 @@
|
|||||||
|
|
||||||
req.send();
|
req.send();
|
||||||
} else {
|
} else {
|
||||||
if (!config['url']) {
|
|
||||||
var scripts = document.getElementsByTagName('script');
|
|
||||||
for (var i = 0; i < scripts.length; i++) {
|
|
||||||
if (scripts[i].src.match(/.*keycloak\.js/)) {
|
|
||||||
config.url = scripts[i].src.substr(0, scripts[i].src.indexOf('/js/keycloak.js'));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.realm) {
|
|
||||||
throw 'realm missing';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.clientId) {
|
if (!config.clientId) {
|
||||||
throw 'clientId missing';
|
throw 'clientId missing';
|
||||||
}
|
}
|
||||||
|
|
||||||
kc.authServerUrl = config.url;
|
|
||||||
kc.realm = config.realm;
|
|
||||||
kc.clientId = config.clientId;
|
kc.clientId = config.clientId;
|
||||||
kc.clientSecret = (config.credentials || {}).secret;
|
kc.clientSecret = (config.credentials || {}).secret;
|
||||||
|
|
||||||
promise.setSuccess();
|
var oidcProvider = config['oidcProvider'];
|
||||||
|
if (!oidcProvider) {
|
||||||
|
if (!config['url']) {
|
||||||
|
var scripts = document.getElementsByTagName('script');
|
||||||
|
for (var i = 0; i < scripts.length; i++) {
|
||||||
|
if (scripts[i].src.match(/.*keycloak\.js/)) {
|
||||||
|
config.url = scripts[i].src.substr(0, scripts[i].src.indexOf('/js/keycloak.js'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!config.realm) {
|
||||||
|
throw 'realm missing';
|
||||||
|
}
|
||||||
|
|
||||||
|
kc.authServerUrl = config.url;
|
||||||
|
kc.realm = config.realm;
|
||||||
|
setupOidcEndoints(null);
|
||||||
|
promise.setSuccess();
|
||||||
|
} else {
|
||||||
|
if (typeof oidcProvider === 'string') {
|
||||||
|
var oidcProviderConfigUrl;
|
||||||
|
if (oidcProvider.charAt(oidcProvider.length - 1) == '/') {
|
||||||
|
oidcProviderConfigUrl = oidcProvider + '.well-known/openid-configuration';
|
||||||
|
} else {
|
||||||
|
oidcProviderConfigUrl = oidcProvider + '/.well-known/openid-configuration';
|
||||||
|
}
|
||||||
|
var req = new XMLHttpRequest();
|
||||||
|
req.open('GET', oidcProviderConfigUrl, true);
|
||||||
|
req.setRequestHeader('Accept', 'application/json');
|
||||||
|
|
||||||
|
req.onreadystatechange = function () {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
if (req.status == 200 || fileLoaded(req)) {
|
||||||
|
var oidcProviderConfig = JSON.parse(req.responseText);
|
||||||
|
setupOidcEndoints(oidcProviderConfig);
|
||||||
|
promise.setSuccess();
|
||||||
|
} else {
|
||||||
|
promise.setError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
req.send();
|
||||||
|
} else {
|
||||||
|
setupOidcEndoints(oidcProvider);
|
||||||
|
promise.setSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise.promise;
|
return promise.promise;
|
||||||
@@ -755,23 +883,137 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseCallback(url) {
|
function parseCallback(url) {
|
||||||
var oauth = new CallbackParser(url, kc.responseMode).parseUri();
|
var oauth = parseCallbackUrl(url);
|
||||||
|
if (!oauth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var oauthState = callbackStorage.get(oauth.state);
|
var oauthState = callbackStorage.get(oauth.state);
|
||||||
|
|
||||||
if (oauthState && (oauth.code || oauth.error || oauth.access_token || oauth.id_token)) {
|
if (oauthState) {
|
||||||
|
oauth.valid = true;
|
||||||
oauth.redirectUri = oauthState.redirectUri;
|
oauth.redirectUri = oauthState.redirectUri;
|
||||||
oauth.storedNonce = oauthState.nonce;
|
oauth.storedNonce = oauthState.nonce;
|
||||||
oauth.prompt = oauthState.prompt;
|
oauth.prompt = oauthState.prompt;
|
||||||
|
}
|
||||||
|
|
||||||
if (oauth.fragment) {
|
return oauth;
|
||||||
oauth.newUrl += '#' + oauth.fragment;
|
}
|
||||||
|
|
||||||
|
function parseCallbackUrl(url) {
|
||||||
|
var supportedParams;
|
||||||
|
switch (kc.flow) {
|
||||||
|
case 'standard':
|
||||||
|
supportedParams = ['code', 'state', 'session_state'];
|
||||||
|
break;
|
||||||
|
case 'implicit':
|
||||||
|
supportedParams = ['access_token', 'id_token', 'state', 'session_state'];
|
||||||
|
break;
|
||||||
|
case 'hybrid':
|
||||||
|
supportedParams = ['access_token', 'id_token', 'code', 'state', 'session_state'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
supportedParams.push('error');
|
||||||
|
supportedParams.push('error_description');
|
||||||
|
supportedParams.push('error_uri');
|
||||||
|
|
||||||
|
var queryIndex = url.indexOf('?');
|
||||||
|
var fragmentIndex = url.indexOf('#');
|
||||||
|
|
||||||
|
var newUrl;
|
||||||
|
var parsed;
|
||||||
|
|
||||||
|
if (kc.responseMode === 'query' && queryIndex !== -1) {
|
||||||
|
newUrl = url.substring(0, queryIndex);
|
||||||
|
parsed = parseCallbackParams(url.substring(queryIndex + 1, fragmentIndex !== -1 ? fragmentIndex : url.length), supportedParams);
|
||||||
|
if (parsed.paramsString !== '') {
|
||||||
|
newUrl += '?' + parsed.paramsString;
|
||||||
}
|
}
|
||||||
|
if (fragmentIndex !== -1) {
|
||||||
|
newUrl += url.substring(fragmentIndex);
|
||||||
|
}
|
||||||
|
} else if (kc.responseMode === 'fragment' && fragmentIndex !== -1) {
|
||||||
|
newUrl = url.substring(0, fragmentIndex);
|
||||||
|
parsed = parseCallbackParams(url.substring(fragmentIndex + 1), supportedParams);
|
||||||
|
if (parsed.paramsString !== '') {
|
||||||
|
newUrl += '#' + parsed.paramsString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return oauth;
|
if (parsed && parsed.oauthParams) {
|
||||||
|
if (kc.flow === 'standard' || kc.flow === 'hybrid') {
|
||||||
|
if ((parsed.oauthParams.code || parsed.oauthParams.error) && parsed.oauthParams.state) {
|
||||||
|
parsed.oauthParams.newUrl = newUrl;
|
||||||
|
return parsed.oauthParams;
|
||||||
|
}
|
||||||
|
} else if (kc.flow === 'implicit') {
|
||||||
|
if ((parsed.oauthParams.access_token || parsed.oauthParams.error) && parsed.oauthParams.state) {
|
||||||
|
parsed.oauthParams.newUrl = newUrl;
|
||||||
|
return parsed.oauthParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseCallbackParams(paramsString, supportedParams) {
|
||||||
|
var p = paramsString.split('&');
|
||||||
|
var result = {
|
||||||
|
paramsString: '',
|
||||||
|
oauthParams: {}
|
||||||
|
}
|
||||||
|
for (var i = 0; i < p.length; i++) {
|
||||||
|
var t = p[i].split('=');
|
||||||
|
if (supportedParams.indexOf(t[0]) !== -1) {
|
||||||
|
result.oauthParams[t[0]] = t[1];
|
||||||
|
} else {
|
||||||
|
if (result.paramsString !== '') {
|
||||||
|
result.paramsString += '&';
|
||||||
|
}
|
||||||
|
result.paramsString += p[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function createPromise() {
|
function createPromise() {
|
||||||
|
if (typeof Promise === "function") {
|
||||||
|
return createNativePromise();
|
||||||
|
} else {
|
||||||
|
return createLegacyPromise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNativePromise() {
|
||||||
|
// Need to create a native Promise which also preserves the
|
||||||
|
// interface of the custom promise type previously used by the API
|
||||||
|
var p = {
|
||||||
|
setSuccess: function(result) {
|
||||||
|
p.success = true;
|
||||||
|
p.resolve(result);
|
||||||
|
},
|
||||||
|
|
||||||
|
setError: function(result) {
|
||||||
|
p.success = false;
|
||||||
|
p.reject(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
p.promise = new Promise(function(resolve, reject) {
|
||||||
|
p.resolve = resolve;
|
||||||
|
p.reject = reject;
|
||||||
|
});
|
||||||
|
p.promise.success = function(callback) {
|
||||||
|
p.promise.then(callback);
|
||||||
|
return p.promise;
|
||||||
|
}
|
||||||
|
p.promise.error = function(callback) {
|
||||||
|
p.promise.catch(callback);
|
||||||
|
return p.promise;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLegacyPromise() {
|
||||||
var p = {
|
var p = {
|
||||||
setSuccess: function(result) {
|
setSuccess: function(result) {
|
||||||
p.success = true;
|
p.success = true;
|
||||||
@@ -828,22 +1070,18 @@
|
|||||||
loginIframe.iframe = iframe;
|
loginIframe.iframe = iframe;
|
||||||
|
|
||||||
iframe.onload = function() {
|
iframe.onload = function() {
|
||||||
var realmUrl = getRealmUrl();
|
var authUrl = kc.endpoints.authorize();
|
||||||
if (realmUrl.charAt(0) === '/') {
|
if (authUrl.charAt(0) === '/') {
|
||||||
loginIframe.iframeOrigin = getOrigin();
|
loginIframe.iframeOrigin = getOrigin();
|
||||||
} else {
|
} else {
|
||||||
loginIframe.iframeOrigin = realmUrl.substring(0, realmUrl.indexOf('/', 8));
|
loginIframe.iframeOrigin = authUrl.substring(0, authUrl.indexOf('/', 8));
|
||||||
}
|
}
|
||||||
promise.setSuccess();
|
promise.setSuccess();
|
||||||
|
|
||||||
setTimeout(check, loginIframe.interval * 1000);
|
setTimeout(check, loginIframe.interval * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html';
|
var src = kc.endpoints.checkSessionIframe();
|
||||||
if (kc.iframeVersion) {
|
|
||||||
src = src + '?version=' + kc.iframeVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
iframe.setAttribute('src', src );
|
iframe.setAttribute('src', src );
|
||||||
iframe.setAttribute('title', 'keycloak-session-iframe' );
|
iframe.setAttribute('title', 'keycloak-session-iframe' );
|
||||||
iframe.style.display = 'none';
|
iframe.style.display = 'none';
|
||||||
@@ -923,7 +1161,12 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
accountManagement : function() {
|
accountManagement : function() {
|
||||||
window.location.href = kc.createAccountUrl();
|
var accountUrl = kc.createAccountUrl();
|
||||||
|
if (typeof accountUrl !== 'undefined') {
|
||||||
|
window.location.href = accountUrl;
|
||||||
|
} else {
|
||||||
|
throw "Not supported by the OIDC server";
|
||||||
|
}
|
||||||
return createPromise().promise;
|
return createPromise().promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -937,12 +1180,7 @@
|
|||||||
} else if (kc.redirectUri) {
|
} else if (kc.redirectUri) {
|
||||||
return kc.redirectUri;
|
return kc.redirectUri;
|
||||||
} else {
|
} else {
|
||||||
var redirectUri = location.href;
|
return location.href;
|
||||||
if (location.hash && encodeHash) {
|
|
||||||
redirectUri = redirectUri.substring(0, location.href.indexOf('#'));
|
|
||||||
redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'redirect_fragment=' + encodeURIComponent(location.hash.substring(1));
|
|
||||||
}
|
|
||||||
return redirectUri;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -958,24 +1196,54 @@
|
|||||||
return window.open(loginUrl, target, options);
|
return window.open(loginUrl, target, options);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var shallowCloneCordovaOptions = function (userOptions) {
|
||||||
|
if (userOptions && userOptions.cordovaOptions) {
|
||||||
|
return Object.keys(userOptions.cordovaOptions).reduce(function (options, optionName) {
|
||||||
|
options[optionName] = userOptions.cordovaOptions[optionName];
|
||||||
|
return options;
|
||||||
|
}, {});
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var formatCordovaOptions = function (cordovaOptions) {
|
||||||
|
return Object.keys(cordovaOptions).reduce(function (options, optionName) {
|
||||||
|
options.push(optionName+"="+cordovaOptions[optionName]);
|
||||||
|
return options;
|
||||||
|
}, []).join(",");
|
||||||
|
};
|
||||||
|
|
||||||
|
var createCordovaOptions = function (userOptions) {
|
||||||
|
var cordovaOptions = shallowCloneCordovaOptions(userOptions);
|
||||||
|
cordovaOptions.location = 'no';
|
||||||
|
if (userOptions && userOptions.prompt == 'none') {
|
||||||
|
cordovaOptions.hidden = 'yes';
|
||||||
|
}
|
||||||
|
return formatCordovaOptions(cordovaOptions);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
login: function(options) {
|
login: function(options) {
|
||||||
var promise = createPromise();
|
var promise = createPromise();
|
||||||
|
|
||||||
var o = 'location=no';
|
var cordovaOptions = createCordovaOptions(options);
|
||||||
if (options && options.prompt == 'none') {
|
|
||||||
o += ',hidden=yes';
|
|
||||||
}
|
|
||||||
|
|
||||||
var loginUrl = kc.createLoginUrl(options);
|
var loginUrl = kc.createLoginUrl(options);
|
||||||
var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', o);
|
var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', cordovaOptions);
|
||||||
var completed = false;
|
var completed = false;
|
||||||
|
|
||||||
|
var closed = false;
|
||||||
|
var closeBrowser = function() {
|
||||||
|
closed = true;
|
||||||
|
ref.close();
|
||||||
|
};
|
||||||
|
|
||||||
ref.addEventListener('loadstart', function(event) {
|
ref.addEventListener('loadstart', function(event) {
|
||||||
if (event.url.indexOf('http://localhost') == 0) {
|
if (event.url.indexOf('http://localhost') == 0) {
|
||||||
var callback = parseCallback(event.url);
|
var callback = parseCallback(event.url);
|
||||||
processCallback(callback, promise);
|
processCallback(callback, promise);
|
||||||
ref.close();
|
closeBrowser();
|
||||||
completed = true;
|
completed = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -985,21 +1253,29 @@
|
|||||||
if (event.url.indexOf('http://localhost') == 0) {
|
if (event.url.indexOf('http://localhost') == 0) {
|
||||||
var callback = parseCallback(event.url);
|
var callback = parseCallback(event.url);
|
||||||
processCallback(callback, promise);
|
processCallback(callback, promise);
|
||||||
ref.close();
|
closeBrowser();
|
||||||
completed = true;
|
completed = true;
|
||||||
} else {
|
} else {
|
||||||
promise.setError();
|
promise.setError();
|
||||||
ref.close();
|
closeBrowser();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ref.addEventListener('exit', function(event) {
|
||||||
|
if (!closed) {
|
||||||
|
promise.setError({
|
||||||
|
reason: "closed_by_user"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return promise.promise;
|
return promise.promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
logout: function(options) {
|
logout: function(options) {
|
||||||
var promise = createPromise();
|
var promise = createPromise();
|
||||||
|
|
||||||
var logoutUrl = kc.createLogoutUrl(options);
|
var logoutUrl = kc.createLogoutUrl(options);
|
||||||
var ref = cordovaOpenWindowWrapper(logoutUrl, '_blank', 'location=no,hidden=yes');
|
var ref = cordovaOpenWindowWrapper(logoutUrl, '_blank', 'location=no,hidden=yes');
|
||||||
|
|
||||||
@@ -1034,7 +1310,8 @@
|
|||||||
|
|
||||||
register : function() {
|
register : function() {
|
||||||
var registerUrl = kc.createRegisterUrl();
|
var registerUrl = kc.createRegisterUrl();
|
||||||
var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', 'location=no');
|
var cordovaOptions = createCordovaOptions(options);
|
||||||
|
var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', cordovaOptions);
|
||||||
ref.addEventListener('loadstart', function(event) {
|
ref.addEventListener('loadstart', function(event) {
|
||||||
if (event.url.indexOf('http://localhost') == 0) {
|
if (event.url.indexOf('http://localhost') == 0) {
|
||||||
ref.close();
|
ref.close();
|
||||||
@@ -1044,12 +1321,16 @@
|
|||||||
|
|
||||||
accountManagement : function() {
|
accountManagement : function() {
|
||||||
var accountUrl = kc.createAccountUrl();
|
var accountUrl = kc.createAccountUrl();
|
||||||
var ref = cordovaOpenWindowWrapper(accountUrl, '_blank', 'location=no');
|
if (typeof accountUrl !== 'undefined') {
|
||||||
ref.addEventListener('loadstart', function(event) {
|
var ref = cordovaOpenWindowWrapper(accountUrl, '_blank', 'location=no');
|
||||||
if (event.url.indexOf('http://localhost') == 0) {
|
ref.addEventListener('loadstart', function(event) {
|
||||||
ref.close();
|
if (event.url.indexOf('http://localhost') == 0) {
|
||||||
}
|
ref.close();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw "Not supported by the OIDC server";
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
redirectUri: function(options) {
|
redirectUri: function(options) {
|
||||||
@@ -1179,99 +1460,6 @@
|
|||||||
|
|
||||||
return new CookieStorage();
|
return new CookieStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
var CallbackParser = function(uriToParse, responseMode) {
|
|
||||||
if (!(this instanceof CallbackParser)) {
|
|
||||||
return new CallbackParser(uriToParse, responseMode);
|
|
||||||
}
|
|
||||||
var parser = this;
|
|
||||||
|
|
||||||
var initialParse = function() {
|
|
||||||
var baseUri = null;
|
|
||||||
var queryString = null;
|
|
||||||
var fragmentString = null;
|
|
||||||
|
|
||||||
var questionMarkIndex = uriToParse.indexOf("?");
|
|
||||||
var fragmentIndex = uriToParse.indexOf("#", questionMarkIndex + 1);
|
|
||||||
if (questionMarkIndex == -1 && fragmentIndex == -1) {
|
|
||||||
baseUri = uriToParse;
|
|
||||||
} else if (questionMarkIndex != -1) {
|
|
||||||
baseUri = uriToParse.substring(0, questionMarkIndex);
|
|
||||||
queryString = uriToParse.substring(questionMarkIndex + 1);
|
|
||||||
if (fragmentIndex != -1) {
|
|
||||||
fragmentIndex = queryString.indexOf("#");
|
|
||||||
fragmentString = queryString.substring(fragmentIndex + 1);
|
|
||||||
queryString = queryString.substring(0, fragmentIndex);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
baseUri = uriToParse.substring(0, fragmentIndex);
|
|
||||||
fragmentString = uriToParse.substring(fragmentIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { baseUri: baseUri, queryString: queryString, fragmentString: fragmentString };
|
|
||||||
}
|
|
||||||
|
|
||||||
var parseParams = function(paramString) {
|
|
||||||
var result = {};
|
|
||||||
var params = paramString.split('&');
|
|
||||||
for (var i = 0; i < params.length; i++) {
|
|
||||||
var p = params[i].split('=');
|
|
||||||
var paramName = decodeURIComponent(p[0]);
|
|
||||||
var paramValue = decodeURIComponent(p[1]);
|
|
||||||
result[paramName] = paramValue;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
var handleQueryParam = function(paramName, paramValue, oauth) {
|
|
||||||
var supportedOAuthParams = [ 'code', 'state', 'error', 'error_description' ];
|
|
||||||
|
|
||||||
for (var i = 0 ; i< supportedOAuthParams.length ; i++) {
|
|
||||||
if (paramName === supportedOAuthParams[i]) {
|
|
||||||
oauth[paramName] = paramValue;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
parser.parseUri = function() {
|
|
||||||
var parsedUri = initialParse();
|
|
||||||
|
|
||||||
var queryParams = {};
|
|
||||||
if (parsedUri.queryString) {
|
|
||||||
queryParams = parseParams(parsedUri.queryString);
|
|
||||||
}
|
|
||||||
|
|
||||||
var oauth = { newUrl: parsedUri.baseUri };
|
|
||||||
for (var param in queryParams) {
|
|
||||||
switch (param) {
|
|
||||||
case 'redirect_fragment':
|
|
||||||
oauth.fragment = queryParams[param];
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (responseMode != 'query' || !handleQueryParam(param, queryParams[param], oauth)) {
|
|
||||||
oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + param + '=' + encodeURIComponent(queryParams[param]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (responseMode === 'fragment') {
|
|
||||||
var fragmentParams = {};
|
|
||||||
if (parsedUri.fragmentString) {
|
|
||||||
fragmentParams = parseParams(parsedUri.fragmentString);
|
|
||||||
}
|
|
||||||
for (var param in fragmentParams) {
|
|
||||||
oauth[param] = fragmentParams[param];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return oauth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
|
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
|
||||||
|
|||||||
@@ -79,6 +79,10 @@
|
|||||||
|
|
||||||
function receiveMessage(event)
|
function receiveMessage(event)
|
||||||
{
|
{
|
||||||
|
if (typeof event.data !== 'string') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var origin = event.origin;
|
var origin = event.origin;
|
||||||
var data = event.data.split(' ');
|
var data = event.data.split(' ');
|
||||||
if (data.length != 2) {
|
if (data.length != 2) {
|
||||||
|
|||||||
File renamed without changes.
@@ -21,12 +21,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<artifactId>keycloak-cli-sso</artifactId>
|
<artifactId>kcinit</artifactId>
|
||||||
<name>Keycloak CLI SSO Framework</name>
|
<name>Keycloak CLI SSO Framework</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<transformers>
|
<transformers>
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
<mainClass>org.keycloak.adapters.KeycloakCliSsoMain</mainClass>
|
<mainClass>org.keycloak.adapters.KcinitMain</mainClass>
|
||||||
</transformer>
|
</transformer>
|
||||||
</transformers>
|
</transformers>
|
||||||
|
|
||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN*)
|
||||||
|
CFILE = `cygpath "$0"`
|
||||||
|
RESOLVED_NAME=`readlink -f "$CFILE"`
|
||||||
|
;;
|
||||||
|
Darwin*)
|
||||||
|
RESOLVED_NAME=`readlink "$0"`
|
||||||
|
;;
|
||||||
|
FreeBSD)
|
||||||
|
RESOLVED_NAME=`readlink -f "$0"`
|
||||||
|
;;
|
||||||
|
Linux)
|
||||||
|
RESOLVED_NAME=`readlink -f "$0"`
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "x$RESOLVED_NAME" = "x" ]; then
|
||||||
|
RESOLVED_NAME="$0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCRIPTPATH=`dirname "$RESOLVED_NAME"`
|
||||||
|
JAR=$SCRIPTPATH/kcinit-${project.version}.jar
|
||||||
|
|
||||||
|
java -jar $JAR $@
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
@echo off
|
||||||
|
|
||||||
|
if "%OS%" == "Windows_NT" (
|
||||||
|
set "DIRNAME=%~dp0%"
|
||||||
|
) else (
|
||||||
|
set DIRNAME=.\
|
||||||
|
)
|
||||||
|
java -jar %DIRNAME%\kcinit-${project.version}.jar %*
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.installed.KcinitDriver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class KcinitMain extends KcinitDriver {
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
new KcinitMain().mainCmd(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
org.keycloak.adapters.osgi.*
|
org.keycloak.adapters.osgi.*
|
||||||
</keycloak.osgi.export>
|
</keycloak.osgi.export>
|
||||||
<keycloak.osgi.import>
|
<keycloak.osgi.import>
|
||||||
org.ops4j.pax.web.*;version="[3.0,5)",
|
org.ops4j.pax.web.*;version="[3.0,8)",
|
||||||
javax.servlet.*;version="[2.5,4)";resolution:=optional,
|
javax.servlet.*;version="[2.5,4)";resolution:=optional,
|
||||||
org.eclipse.jetty.*;version="[8.1,10)";resolution:=optional,
|
org.eclipse.jetty.*;version="[8.1,10)";resolution:=optional,
|
||||||
org.keycloak.*;version="${project.version}",
|
org.keycloak.*;version="${project.version}",
|
||||||
@@ -99,6 +99,12 @@
|
|||||||
<version>${cxf.version}</version>
|
<version>${cxf.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
+31
-7
@@ -25,6 +25,7 @@ import java.io.File;
|
|||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@@ -34,13 +35,35 @@ public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
|
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
|
||||||
String path = request.getURI();
|
String uri = request.getURI();
|
||||||
String[] urlTokens = path.split("/");
|
String relativePath = request.getRelativePath();
|
||||||
if (urlTokens.length < 4) {
|
String webContext = null;
|
||||||
throw new IllegalStateException("Not able to determine the web-context to load the correspondent keycloak.json file");
|
if (relativePath == null || !uri.contains(relativePath)) {
|
||||||
}
|
String[] urlTokens = uri.split("/");
|
||||||
|
if (urlTokens.length < 4) {
|
||||||
|
throw new IllegalStateException("Not able to determine the web-context to load the correspondent keycloak.json file");
|
||||||
|
}
|
||||||
|
|
||||||
String webContext = urlTokens[3];
|
webContext = urlTokens[3];
|
||||||
|
} else {
|
||||||
|
URI parsedURI = URI.create(uri);
|
||||||
|
String path = parsedURI.getPath();
|
||||||
|
path = path.substring(0, path.indexOf(relativePath));
|
||||||
|
while (path.startsWith("/")) {
|
||||||
|
path = path.substring(1);
|
||||||
|
}
|
||||||
|
webContext = path;
|
||||||
|
if ("".equals(webContext)) {
|
||||||
|
path = relativePath;
|
||||||
|
while (path.startsWith("/")) {
|
||||||
|
path = path.substring(1);
|
||||||
|
}
|
||||||
|
if (path.contains("/")) {
|
||||||
|
path = path.substring(0, path.indexOf("/"));
|
||||||
|
}
|
||||||
|
webContext = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
KeycloakDeployment deployment = cache.get(webContext);
|
KeycloakDeployment deployment = cache.get(webContext);
|
||||||
if (null == deployment) {
|
if (null == deployment) {
|
||||||
@@ -54,7 +77,8 @@ public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
|
|||||||
keycloakConfig = karafEtc;
|
keycloakConfig = karafEtc;
|
||||||
}
|
}
|
||||||
|
|
||||||
String absolutePath = keycloakConfig + File.separator + webContext + "-keycloak.json";
|
String absolutePath = keycloakConfig + File.separator + webContext + ("".equals(webContext) ? "" : "-")
|
||||||
|
+ "keycloak.json";
|
||||||
InputStream is = null;
|
InputStream is = null;
|
||||||
try {
|
try {
|
||||||
is = new FileInputStream(absolutePath);
|
is = new FileInputStream(absolutePath);
|
||||||
|
|||||||
+102
-7
@@ -29,11 +29,12 @@ import org.osgi.util.tracker.ServiceTrackerCustomizer;
|
|||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integration with pax-web, which allows to inject custom jetty-web.xml configuration from current bundle classpath into {@link WebContainer}
|
* Integration with pax-web in Fuse 6.3, which allows to inject custom jetty-web.xml configuration from current bundle classpath into {@link WebContainer}
|
||||||
* and allows to inject custom security constraint for securing resources by Keycloak.
|
* and allows to inject custom security constraint for securing resources by Keycloak.
|
||||||
*
|
*
|
||||||
* <p>It assumes that pax-web {@link WebContainer} is used as implementation of OSGI {@link org.osgi.service.http.HttpService}, which
|
* <p>It assumes that pax-web {@link WebContainer} is used as implementation of OSGI {@link org.osgi.service.http.HttpService}, which
|
||||||
@@ -47,7 +48,7 @@ public class PaxWebIntegrationService {
|
|||||||
|
|
||||||
private BundleContext bundleContext;
|
private BundleContext bundleContext;
|
||||||
private String jettyWebXmlLocation;
|
private String jettyWebXmlLocation;
|
||||||
private List<ConstraintMapping> constraintMappings; // Using jetty constraint mapping just because of compatibility with other fuse services
|
private List<Object> constraintMappings;
|
||||||
|
|
||||||
private ServiceTracker webContainerTracker;
|
private ServiceTracker webContainerTracker;
|
||||||
private HttpContext httpContext;
|
private HttpContext httpContext;
|
||||||
@@ -68,11 +69,11 @@ public class PaxWebIntegrationService {
|
|||||||
this.jettyWebXmlLocation = jettyWebXmlLocation;
|
this.jettyWebXmlLocation = jettyWebXmlLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ConstraintMapping> getConstraintMappings() {
|
public List<Object> getConstraintMappings() {
|
||||||
return constraintMappings;
|
return constraintMappings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setConstraintMappings(List<ConstraintMapping> constraintMappings) {
|
public void setConstraintMappings(List<Object> constraintMappings) {
|
||||||
this.constraintMappings = constraintMappings;
|
this.constraintMappings = constraintMappings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,8 +121,25 @@ public class PaxWebIntegrationService {
|
|||||||
if (constraintMappings == null) {
|
if (constraintMappings == null) {
|
||||||
throw new IllegalStateException("constraintMappings was null!");
|
throw new IllegalStateException("constraintMappings was null!");
|
||||||
}
|
}
|
||||||
for (ConstraintMapping constraintMapping : constraintMappings) {
|
List<ConstraintHandler> handlers = new ArrayList<>();
|
||||||
addConstraintMapping(service, constraintMapping);
|
try {
|
||||||
|
handlers.add(new JettyConstraintHandler());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
handlers.add(new PaxWebConstraintHandler());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
for (Object constraintMapping : constraintMappings) {
|
||||||
|
boolean handled = false;
|
||||||
|
for (ConstraintHandler handler : handlers) {
|
||||||
|
handled |= handler.addConstraintMapping(httpContext, service, constraintMapping);
|
||||||
|
}
|
||||||
|
if (!handled) {
|
||||||
|
log.warnv("Unable to add constraint mapping for constraint of type " + constraintMapping.getClass().toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
service.registerLoginConfig("BASIC", "does-not-matter", null, null, httpContext);
|
service.registerLoginConfig("BASIC", "does-not-matter", null, null, httpContext);
|
||||||
@@ -146,6 +164,16 @@ public class PaxWebIntegrationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void addConstraintMapping(WebContainer service, PaxWebSecurityConstraintMapping constraintMapping) {
|
||||||
|
String name = constraintMapping.getConstraintName();
|
||||||
|
if (name == null) {
|
||||||
|
name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getUrl() + ", dataConstraint=" + constraintMapping.getDataConstraint() + ", canAuthenticate="
|
||||||
|
+ constraintMapping.isAuthentication() + ", roles=" + constraintMapping.getRoles());
|
||||||
|
service.registerConstraintMapping(name, constraintMapping.getUrl(), constraintMapping.getMapping(), constraintMapping.getDataConstraint(), constraintMapping.isAuthentication(), constraintMapping.getRoles(), httpContext);
|
||||||
|
}
|
||||||
|
|
||||||
protected void addConstraintMapping(WebContainer service, ConstraintMapping constraintMapping) {
|
protected void addConstraintMapping(WebContainer service, ConstraintMapping constraintMapping) {
|
||||||
Constraint constraint = constraintMapping.getConstraint();
|
Constraint constraint = constraintMapping.getConstraint();
|
||||||
String[] roles = constraint.getRoles();
|
String[] roles = constraint.getRoles();
|
||||||
@@ -178,4 +206,71 @@ public class PaxWebIntegrationService {
|
|||||||
service.unregisterConstraintMapping(httpContext);
|
service.unregisterConstraintMapping(httpContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private interface ConstraintHandler {
|
||||||
|
boolean addConstraintMapping(HttpContext httpContext, WebContainer service, Object cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PaxWebConstraintHandler implements ConstraintHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean addConstraintMapping(HttpContext httpContext, WebContainer service, Object cm) {
|
||||||
|
if (cm instanceof PaxWebSecurityConstraintMapping) {
|
||||||
|
PaxWebSecurityConstraintMapping constraintMapping = (PaxWebSecurityConstraintMapping) cm;
|
||||||
|
String name = constraintMapping.getConstraintName();
|
||||||
|
if (name == null) {
|
||||||
|
name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getUrl() + ", dataConstraint=" + constraintMapping.getDataConstraint() + ", canAuthenticate="
|
||||||
|
+ constraintMapping.isAuthentication() + ", roles=" + constraintMapping.getRoles());
|
||||||
|
service.registerConstraintMapping(name, constraintMapping.getUrl(), constraintMapping.getMapping(), constraintMapping.getDataConstraint(), constraintMapping.isAuthentication(), constraintMapping.getRoles(), httpContext);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class JettyConstraintHandler implements ConstraintHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean addConstraintMapping(HttpContext httpContext, WebContainer service, Object cm) {
|
||||||
|
if (cm instanceof ConstraintMapping) {
|
||||||
|
ConstraintMapping constraintMapping = (ConstraintMapping) cm;
|
||||||
|
Constraint constraint = constraintMapping.getConstraint();
|
||||||
|
String[] roles = constraint.getRoles();
|
||||||
|
// name property is unavailable on constraint object :/
|
||||||
|
|
||||||
|
String name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
|
||||||
|
int dataConstraint = constraint.getDataConstraint();
|
||||||
|
String dataConstraintStr;
|
||||||
|
switch (dataConstraint) {
|
||||||
|
case Constraint.DC_UNSET:
|
||||||
|
dataConstraintStr = null;
|
||||||
|
break;
|
||||||
|
case Constraint.DC_NONE:
|
||||||
|
dataConstraintStr = "NONE";
|
||||||
|
break;
|
||||||
|
case Constraint.DC_CONFIDENTIAL:
|
||||||
|
dataConstraintStr = "CONFIDENTIAL";
|
||||||
|
break;
|
||||||
|
case Constraint.DC_INTEGRAL:
|
||||||
|
dataConstraintStr = "INTEGRAL";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.warnv("Unknown data constraint: " + dataConstraint);
|
||||||
|
dataConstraintStr = "CONFIDENTIAL";
|
||||||
|
}
|
||||||
|
List<String> rolesList = Arrays.asList(roles);
|
||||||
|
|
||||||
|
log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getPathSpec() + ", dataConstraint=" + dataConstraintStr + ", canAuthenticate="
|
||||||
|
+ constraint.getAuthenticate() + ", roles=" + rolesList);
|
||||||
|
service.registerConstraintMapping(name, constraintMapping.getPathSpec(), null, dataConstraintStr, constraint.getAuthenticate(), rolesList, httpContext);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* To change this license header, choose License Headers in Project Properties.
|
||||||
|
* To change this template file, choose Tools | Templates
|
||||||
|
* and open the template in the editor.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.osgi;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class PaxWebSecurityConstraintMapping {
|
||||||
|
|
||||||
|
private String constraintName = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
private String mapping;
|
||||||
|
private String url;
|
||||||
|
private String dataConstraint = "NONE";
|
||||||
|
private boolean authentication = true;
|
||||||
|
private List<String> roles = new LinkedList<>();
|
||||||
|
|
||||||
|
public String getConstraintName() {
|
||||||
|
return constraintName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConstraintName(String constraintName) {
|
||||||
|
this.constraintName = constraintName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMapping() {
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMapping(String mapping) {
|
||||||
|
this.mapping = mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUrl(String url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDataConstraint() {
|
||||||
|
return dataConstraint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDataConstraint(String dataConstraint) {
|
||||||
|
this.dataConstraint = dataConstraint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAuthentication() {
|
||||||
|
return authentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthentication(boolean authentication) {
|
||||||
|
this.authentication = authentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getRoles() {
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRoles(List<String> roles) {
|
||||||
|
this.roles = roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+149
@@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.osgi;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.OIDCHttpFacade;
|
||||||
|
import org.keycloak.adapters.spi.AuthenticationError;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.adapters.spi.LogoutError;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
|
public class PathBasedKeycloakConfigResolverTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void relativeURIsAndContexts() throws Exception {
|
||||||
|
PathBasedKeycloakConfigResolver resolver = new PathBasedKeycloakConfigResolver();
|
||||||
|
|
||||||
|
assertNotNull(populate(resolver, "test")
|
||||||
|
.resolve(new MockRequest("http://localhost/test/a/b/c?d=e", "/a/b/c")));
|
||||||
|
|
||||||
|
assertNotNull(populate(resolver, "test")
|
||||||
|
.resolve(new MockRequest("http://localhost/test/a/b/c?d=e", "/a/b")));
|
||||||
|
|
||||||
|
// means default context and actually we use first segment
|
||||||
|
assertNotNull(populate(resolver, "test")
|
||||||
|
.resolve(new MockRequest("http://localhost/test/a/b/c?d=e", "/test/a/b/c")));
|
||||||
|
|
||||||
|
assertNotNull(populate(resolver, "test/a")
|
||||||
|
.resolve(new MockRequest("http://localhost/test/a/b/c?d=e", "/b/c")));
|
||||||
|
|
||||||
|
assertNotNull(populate(resolver, "")
|
||||||
|
.resolve(new MockRequest("http://localhost/", "/")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private PathBasedKeycloakConfigResolver populate(PathBasedKeycloakConfigResolver resolver, String context)
|
||||||
|
throws Exception {
|
||||||
|
Field f = PathBasedKeycloakConfigResolver.class.getDeclaredField("cache");
|
||||||
|
f.setAccessible(true);
|
||||||
|
Map<String, KeycloakDeployment> cache = (Map<String, KeycloakDeployment>) f.get(resolver);
|
||||||
|
cache.clear();
|
||||||
|
cache.put(context, new KeycloakDeployment());
|
||||||
|
|
||||||
|
return resolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MockRequest implements OIDCHttpFacade.Request {
|
||||||
|
|
||||||
|
private String uri;
|
||||||
|
private String relativePath;
|
||||||
|
|
||||||
|
public MockRequest(String uri, String relativePath) {
|
||||||
|
this.uri = uri;
|
||||||
|
this.relativePath = relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMethod() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getURI() {
|
||||||
|
return this.uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRelativePath() {
|
||||||
|
return this.relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSecure() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFirstParam(String param) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getQueryParamValue(String param) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpFacade.Cookie getCookie(String cookieName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHeader(String name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getHeaders(String name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream(boolean buffered) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRemoteAddr() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setError(AuthenticationError error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setError(LogoutError error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../pom.xml</relativePath>
|
<relativePath>../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<name>Keycloak OIDC Client Adapter Modules</name>
|
<name>Keycloak OIDC Client Adapter Modules</name>
|
||||||
@@ -34,7 +34,8 @@
|
|||||||
<module>adapter-core</module>
|
<module>adapter-core</module>
|
||||||
<module>as7-eap6</module>
|
<module>as7-eap6</module>
|
||||||
<module>installed</module>
|
<module>installed</module>
|
||||||
<module>cli-sso</module>
|
<module>fuse7</module>
|
||||||
|
<module>kcinit</module>
|
||||||
<module>jaxrs-oauth-client</module>
|
<module>jaxrs-oauth-client</module>
|
||||||
<module>jetty</module>
|
<module>jetty</module>
|
||||||
<module>js</module>
|
<module>js</module>
|
||||||
@@ -42,6 +43,8 @@
|
|||||||
<module>servlet-filter</module>
|
<module>servlet-filter</module>
|
||||||
<module>servlet-oauth-client</module>
|
<module>servlet-oauth-client</module>
|
||||||
<module>spring-boot</module>
|
<module>spring-boot</module>
|
||||||
|
<module>spring-boot2</module>
|
||||||
|
<module>spring-boot-adapter-core</module>
|
||||||
<module>spring-boot-container-bundle</module>
|
<module>spring-boot-container-bundle</module>
|
||||||
<module>spring-security</module>
|
<module>spring-security</module>
|
||||||
<module>tomcat</module>
|
<module>tomcat</module>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+23
-2
@@ -36,6 +36,8 @@ import javax.security.cert.X509Certificate;
|
|||||||
import javax.servlet.http.Cookie;
|
import javax.servlet.http.Cookie;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@@ -265,6 +267,8 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
|||||||
public Request getRequest() {
|
public Request getRequest() {
|
||||||
return new Request() {
|
return new Request() {
|
||||||
|
|
||||||
|
private InputStream inputStream;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFirstParam(String param) {
|
public String getFirstParam(String param) {
|
||||||
return servletRequest.getParameter(param);
|
return servletRequest.getParameter(param);
|
||||||
@@ -314,10 +318,27 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream getInputStream() {
|
public InputStream getInputStream() {
|
||||||
|
return getInputStream(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream(boolean buffered) {
|
||||||
|
if (inputStream != null) {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffered) {
|
||||||
|
try {
|
||||||
|
return inputStream = new BufferedInputStream(servletRequest.getInputStream());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return servletRequest.getInputStream();
|
return servletRequest.getInputStream();
|
||||||
} catch (IOException ioe) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(ioe);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-parent</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>4.0.0.Final</version>
|
||||||
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-spring-boot-adapter-core</artifactId>
|
||||||
|
<name>Keycloak Spring Boot Adapter Core</name>
|
||||||
|
<description/>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<spring-boot.version>1.3.0.RELEASE</spring-boot.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.logging</groupId>
|
||||||
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>spring-boot-container-bundle</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<optional>true</optional>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-spring-security-adapter</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.undertow</groupId>
|
||||||
|
<artifactId>undertow-servlet</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-server</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-security</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-webapp</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+262
@@ -0,0 +1,262 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.springboot;
|
||||||
|
|
||||||
|
import io.undertow.servlet.api.DeploymentInfo;
|
||||||
|
import io.undertow.servlet.api.WebResourceCollection;
|
||||||
|
import org.apache.catalina.Context;
|
||||||
|
import org.apache.tomcat.util.descriptor.web.LoginConfig;
|
||||||
|
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
|
||||||
|
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
|
||||||
|
import org.eclipse.jetty.security.ConstraintMapping;
|
||||||
|
import org.eclipse.jetty.security.ConstraintSecurityHandler;
|
||||||
|
import org.eclipse.jetty.server.Server;
|
||||||
|
import org.eclipse.jetty.util.security.Constraint;
|
||||||
|
import org.eclipse.jetty.webapp.WebAppContext;
|
||||||
|
import org.keycloak.adapters.jetty.KeycloakJettyAuthenticator;
|
||||||
|
import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
|
||||||
|
import org.keycloak.adapters.undertow.KeycloakServletExtension;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||||
|
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
|
||||||
|
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
|
||||||
|
import org.springframework.boot.context.embedded.jetty.JettyEmbeddedServletContainerFactory;
|
||||||
|
import org.springframework.boot.context.embedded.jetty.JettyServerCustomizer;
|
||||||
|
import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
|
||||||
|
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
|
||||||
|
import org.springframework.boot.context.embedded.undertow.UndertowDeploymentInfoCustomizer;
|
||||||
|
import org.springframework.boot.context.embedded.undertow.UndertowEmbeddedServletContainerFactory;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keycloak authentication base integration for Spring Boot - base to be extended for particular boot versions.
|
||||||
|
*/
|
||||||
|
public class KeycloakBaseSpringBootConfiguration {
|
||||||
|
|
||||||
|
protected KeycloakSpringBootProperties keycloakProperties;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setKeycloakSpringBootProperties(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
|
this.keycloakProperties = keycloakProperties;
|
||||||
|
KeycloakSpringBootConfigResolver.setAdapterConfig(keycloakProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class KeycloakBaseUndertowDeploymentInfoCustomizer {
|
||||||
|
|
||||||
|
protected final KeycloakSpringBootProperties keycloakProperties;
|
||||||
|
|
||||||
|
public KeycloakBaseUndertowDeploymentInfoCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
|
this.keycloakProperties = keycloakProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void customize(DeploymentInfo deploymentInfo) {
|
||||||
|
|
||||||
|
io.undertow.servlet.api.LoginConfig loginConfig = new io.undertow.servlet.api.LoginConfig(keycloakProperties.getRealm());
|
||||||
|
loginConfig.addFirstAuthMethod("KEYCLOAK");
|
||||||
|
|
||||||
|
deploymentInfo.setLoginConfig(loginConfig);
|
||||||
|
|
||||||
|
deploymentInfo.addInitParameter("keycloak.config.resolver", KeycloakSpringBootConfigResolver.class.getName());
|
||||||
|
deploymentInfo.addSecurityConstraints(getSecurityConstraints());
|
||||||
|
|
||||||
|
deploymentInfo.addServletExtension(new KeycloakServletExtension());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<io.undertow.servlet.api.SecurityConstraint> getSecurityConstraints() {
|
||||||
|
|
||||||
|
List<io.undertow.servlet.api.SecurityConstraint> undertowSecurityConstraints = new ArrayList<io.undertow.servlet.api.SecurityConstraint>();
|
||||||
|
for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) {
|
||||||
|
|
||||||
|
io.undertow.servlet.api.SecurityConstraint undertowSecurityConstraint = new io.undertow.servlet.api.SecurityConstraint();
|
||||||
|
undertowSecurityConstraint.addRolesAllowed(constraintDefinition.getAuthRoles());
|
||||||
|
|
||||||
|
for (KeycloakSpringBootProperties.SecurityCollection collectionDefinition : constraintDefinition.getSecurityCollections()) {
|
||||||
|
|
||||||
|
WebResourceCollection webResourceCollection = new WebResourceCollection();
|
||||||
|
webResourceCollection.addHttpMethods(collectionDefinition.getMethods());
|
||||||
|
webResourceCollection.addHttpMethodOmissions(collectionDefinition.getOmittedMethods());
|
||||||
|
webResourceCollection.addUrlPatterns(collectionDefinition.getPatterns());
|
||||||
|
|
||||||
|
undertowSecurityConstraint.addWebResourceCollections(webResourceCollection);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
undertowSecurityConstraints.add(undertowSecurityConstraint);
|
||||||
|
}
|
||||||
|
return undertowSecurityConstraints;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class KeycloakBaseJettyServerCustomizer {
|
||||||
|
|
||||||
|
protected final KeycloakSpringBootProperties keycloakProperties;
|
||||||
|
|
||||||
|
public KeycloakBaseJettyServerCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
|
this.keycloakProperties = keycloakProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void customize(Server server) {
|
||||||
|
|
||||||
|
KeycloakJettyAuthenticator keycloakJettyAuthenticator = new KeycloakJettyAuthenticator();
|
||||||
|
keycloakJettyAuthenticator.setConfigResolver(new KeycloakSpringBootConfigResolver());
|
||||||
|
|
||||||
|
/* see org.eclipse.jetty.webapp.StandardDescriptorProcessor#visitSecurityConstraint for an example
|
||||||
|
on how to map servlet spec to Constraints */
|
||||||
|
|
||||||
|
List<ConstraintMapping> jettyConstraintMappings = new ArrayList<ConstraintMapping>();
|
||||||
|
for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) {
|
||||||
|
|
||||||
|
for (KeycloakSpringBootProperties.SecurityCollection securityCollectionDefinition : constraintDefinition
|
||||||
|
.getSecurityCollections()) {
|
||||||
|
// securityCollection matches servlet spec's web-resource-collection
|
||||||
|
Constraint jettyConstraint = new Constraint();
|
||||||
|
|
||||||
|
if (constraintDefinition.getAuthRoles().size() > 0) {
|
||||||
|
jettyConstraint.setAuthenticate(true);
|
||||||
|
jettyConstraint.setRoles(constraintDefinition.getAuthRoles().toArray(new String[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
jettyConstraint.setName(securityCollectionDefinition.getName());
|
||||||
|
|
||||||
|
// according to the servlet spec each security-constraint has at least one URL pattern
|
||||||
|
for(String pattern : securityCollectionDefinition.getPatterns()) {
|
||||||
|
|
||||||
|
/* the following code is asymmetric as Jetty's ConstraintMapping accepts only one allowed HTTP method,
|
||||||
|
but multiple omitted methods. Therefore we add one ConstraintMapping for each allowed
|
||||||
|
mapping but only one mapping in the cases of omitted methods or no methods.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (securityCollectionDefinition.getMethods().size() > 0) {
|
||||||
|
// according to the servlet spec we have either methods ...
|
||||||
|
for(String method : securityCollectionDefinition.getMethods()) {
|
||||||
|
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
||||||
|
jettyConstraintMappings.add(jettyConstraintMapping);
|
||||||
|
|
||||||
|
jettyConstraintMapping.setConstraint(jettyConstraint);
|
||||||
|
jettyConstraintMapping.setPathSpec(pattern);
|
||||||
|
jettyConstraintMapping.setMethod(method);
|
||||||
|
}
|
||||||
|
} else if (securityCollectionDefinition.getOmittedMethods().size() > 0){
|
||||||
|
// ... omitted methods ...
|
||||||
|
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
||||||
|
jettyConstraintMappings.add(jettyConstraintMapping);
|
||||||
|
|
||||||
|
jettyConstraintMapping.setConstraint(jettyConstraint);
|
||||||
|
jettyConstraintMapping.setPathSpec(pattern);
|
||||||
|
jettyConstraintMapping.setMethodOmissions(
|
||||||
|
securityCollectionDefinition.getOmittedMethods().toArray(new String[0]));
|
||||||
|
} else {
|
||||||
|
// ... or no methods at all
|
||||||
|
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
||||||
|
jettyConstraintMappings.add(jettyConstraintMapping);
|
||||||
|
|
||||||
|
jettyConstraintMapping.setConstraint(jettyConstraint);
|
||||||
|
jettyConstraintMapping.setPathSpec(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WebAppContext webAppContext = server.getBean(WebAppContext.class);
|
||||||
|
//if not found as registered bean let's try the handler
|
||||||
|
if(webAppContext==null){
|
||||||
|
webAppContext = (WebAppContext) server.getHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
|
||||||
|
securityHandler.setConstraintMappings(jettyConstraintMappings);
|
||||||
|
securityHandler.setAuthenticator(keycloakJettyAuthenticator);
|
||||||
|
|
||||||
|
webAppContext.setSecurityHandler(securityHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class KeycloakBaseTomcatContextCustomizer {
|
||||||
|
|
||||||
|
protected final KeycloakSpringBootProperties keycloakProperties;
|
||||||
|
|
||||||
|
public KeycloakBaseTomcatContextCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
|
this.keycloakProperties = keycloakProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void customize(Context context) {
|
||||||
|
LoginConfig loginConfig = new LoginConfig();
|
||||||
|
loginConfig.setAuthMethod("KEYCLOAK");
|
||||||
|
context.setLoginConfig(loginConfig);
|
||||||
|
|
||||||
|
Set<String> authRoles = new HashSet<String>();
|
||||||
|
for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) {
|
||||||
|
for (String authRole : constraint.getAuthRoles()) {
|
||||||
|
if (!authRoles.contains(authRole)) {
|
||||||
|
context.addSecurityRole(authRole);
|
||||||
|
authRoles.add(authRole);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) {
|
||||||
|
SecurityConstraint tomcatConstraint = new SecurityConstraint();
|
||||||
|
|
||||||
|
for (String authRole : constraint.getAuthRoles()) {
|
||||||
|
tomcatConstraint.addAuthRole(authRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (KeycloakSpringBootProperties.SecurityCollection collection : constraint.getSecurityCollections()) {
|
||||||
|
SecurityCollection tomcatSecCollection = new SecurityCollection();
|
||||||
|
|
||||||
|
if (collection.getName() != null) {
|
||||||
|
tomcatSecCollection.setName(collection.getName());
|
||||||
|
}
|
||||||
|
if (collection.getDescription() != null) {
|
||||||
|
tomcatSecCollection.setDescription(collection.getDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String pattern : collection.getPatterns()) {
|
||||||
|
tomcatSecCollection.addPattern(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String method : collection.getMethods()) {
|
||||||
|
tomcatSecCollection.addMethod(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String method : collection.getOmittedMethods()) {
|
||||||
|
tomcatSecCollection.addOmittedMethod(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
tomcatConstraint.addCollection(tomcatSecCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.addConstraint(tomcatConstraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.addParameter("keycloak.config.resolver", KeycloakSpringBootConfigResolver.class.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,2 @@
|
|||||||
|
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||||
|
org.keycloak.adapters.springboot.KeycloakAutoConfiguration
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>spring-boot-container-bundle</artifactId>
|
<artifactId>spring-boot-container-bundle</artifactId>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
@@ -37,6 +37,12 @@
|
|||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-spring-boot-adapter-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jboss.logging</groupId>
|
<groupId>org.jboss.logging</groupId>
|
||||||
<artifactId>jboss-logging</artifactId>
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
|||||||
+7
-199
@@ -17,20 +17,7 @@
|
|||||||
|
|
||||||
package org.keycloak.adapters.springboot;
|
package org.keycloak.adapters.springboot;
|
||||||
|
|
||||||
import io.undertow.servlet.api.DeploymentInfo;
|
|
||||||
import io.undertow.servlet.api.WebResourceCollection;
|
|
||||||
import org.apache.catalina.Context;
|
|
||||||
import org.apache.tomcat.util.descriptor.web.LoginConfig;
|
|
||||||
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
|
|
||||||
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
|
|
||||||
import org.eclipse.jetty.security.ConstraintMapping;
|
|
||||||
import org.eclipse.jetty.security.ConstraintSecurityHandler;
|
|
||||||
import org.eclipse.jetty.server.Server;
|
|
||||||
import org.eclipse.jetty.util.security.Constraint;
|
|
||||||
import org.eclipse.jetty.webapp.WebAppContext;
|
|
||||||
import org.keycloak.adapters.jetty.KeycloakJettyAuthenticator;
|
|
||||||
import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
|
import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
|
||||||
import org.keycloak.adapters.undertow.KeycloakServletExtension;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
@@ -47,10 +34,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keycloak authentication integration for Spring Boot
|
* Keycloak authentication integration for Spring Boot
|
||||||
@@ -62,7 +45,7 @@ import java.util.Set;
|
|||||||
@ConditionalOnWebApplication
|
@ConditionalOnWebApplication
|
||||||
@EnableConfigurationProperties(KeycloakSpringBootProperties.class)
|
@EnableConfigurationProperties(KeycloakSpringBootProperties.class)
|
||||||
@ConditionalOnProperty(value = "keycloak.enabled", matchIfMissing = true)
|
@ConditionalOnProperty(value = "keycloak.enabled", matchIfMissing = true)
|
||||||
public class KeycloakAutoConfiguration {
|
public class KeycloakAutoConfiguration extends KeycloakBaseSpringBootConfiguration {
|
||||||
|
|
||||||
private KeycloakSpringBootProperties keycloakProperties;
|
private KeycloakSpringBootProperties keycloakProperties;
|
||||||
|
|
||||||
@@ -117,202 +100,27 @@ public class KeycloakAutoConfiguration {
|
|||||||
return new KeycloakUndertowDeploymentInfoCustomizer(keycloakProperties);
|
return new KeycloakUndertowDeploymentInfoCustomizer(keycloakProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
static class KeycloakUndertowDeploymentInfoCustomizer implements UndertowDeploymentInfoCustomizer {
|
static class KeycloakUndertowDeploymentInfoCustomizer extends KeycloakBaseUndertowDeploymentInfoCustomizer implements UndertowDeploymentInfoCustomizer {
|
||||||
|
|
||||||
private final KeycloakSpringBootProperties keycloakProperties;
|
|
||||||
|
|
||||||
public KeycloakUndertowDeploymentInfoCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
public KeycloakUndertowDeploymentInfoCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
this.keycloakProperties = keycloakProperties;
|
super(keycloakProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void customize(DeploymentInfo deploymentInfo) {
|
|
||||||
|
|
||||||
io.undertow.servlet.api.LoginConfig loginConfig = new io.undertow.servlet.api.LoginConfig(keycloakProperties.getRealm());
|
|
||||||
loginConfig.addFirstAuthMethod("KEYCLOAK");
|
|
||||||
|
|
||||||
deploymentInfo.setLoginConfig(loginConfig);
|
|
||||||
deploymentInfo.addInitParameter("keycloak.config.resolver", KeycloakSpringBootConfigResolver.class.getName());
|
|
||||||
deploymentInfo.addSecurityConstraints(getSecurityConstraints());
|
|
||||||
deploymentInfo.addServletExtension(new KeycloakServletExtension());
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<io.undertow.servlet.api.SecurityConstraint> getSecurityConstraints() {
|
|
||||||
|
|
||||||
List<io.undertow.servlet.api.SecurityConstraint> undertowSecurityConstraints = new ArrayList<io.undertow.servlet.api.SecurityConstraint>();
|
|
||||||
for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) {
|
|
||||||
|
|
||||||
io.undertow.servlet.api.SecurityConstraint undertowSecurityConstraint = new io.undertow.servlet.api.SecurityConstraint();
|
|
||||||
undertowSecurityConstraint.addRolesAllowed(constraintDefinition.getAuthRoles());
|
|
||||||
|
|
||||||
for (KeycloakSpringBootProperties.SecurityCollection collectionDefinition : constraintDefinition.getSecurityCollections()) {
|
|
||||||
|
|
||||||
WebResourceCollection webResourceCollection = new WebResourceCollection();
|
|
||||||
webResourceCollection.addHttpMethods(collectionDefinition.getMethods());
|
|
||||||
webResourceCollection.addHttpMethodOmissions(collectionDefinition.getOmittedMethods());
|
|
||||||
webResourceCollection.addUrlPatterns(collectionDefinition.getPatterns());
|
|
||||||
|
|
||||||
undertowSecurityConstraint.addWebResourceCollections(webResourceCollection);
|
|
||||||
|
|
||||||
}
|
|
||||||
undertowSecurityConstraints.add(undertowSecurityConstraint);
|
|
||||||
}
|
|
||||||
return undertowSecurityConstraints;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static class KeycloakJettyServerCustomizer implements JettyServerCustomizer {
|
static class KeycloakJettyServerCustomizer extends KeycloakBaseJettyServerCustomizer implements JettyServerCustomizer {
|
||||||
|
|
||||||
private final KeycloakSpringBootProperties keycloakProperties;
|
|
||||||
|
|
||||||
public KeycloakJettyServerCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
public KeycloakJettyServerCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
this.keycloakProperties = keycloakProperties;
|
super(keycloakProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void customize(Server server) {
|
|
||||||
|
|
||||||
KeycloakJettyAuthenticator keycloakJettyAuthenticator = new KeycloakJettyAuthenticator();
|
|
||||||
keycloakJettyAuthenticator.setConfigResolver(new KeycloakSpringBootConfigResolver());
|
|
||||||
|
|
||||||
/* see org.eclipse.jetty.webapp.StandardDescriptorProcessor#visitSecurityConstraint for an example
|
|
||||||
on how to map servlet spec to Constraints */
|
|
||||||
|
|
||||||
List<ConstraintMapping> jettyConstraintMappings = new ArrayList<ConstraintMapping>();
|
|
||||||
for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) {
|
|
||||||
|
|
||||||
for (KeycloakSpringBootProperties.SecurityCollection securityCollectionDefinition : constraintDefinition
|
|
||||||
.getSecurityCollections()) {
|
|
||||||
// securityCollection matches servlet spec's web-resource-collection
|
|
||||||
Constraint jettyConstraint = new Constraint();
|
|
||||||
|
|
||||||
if (constraintDefinition.getAuthRoles().size() > 0) {
|
|
||||||
jettyConstraint.setAuthenticate(true);
|
|
||||||
jettyConstraint.setRoles(constraintDefinition.getAuthRoles().toArray(new String[0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
jettyConstraint.setName(securityCollectionDefinition.getName());
|
|
||||||
|
|
||||||
// according to the servlet spec each security-constraint has at least one URL pattern
|
|
||||||
for(String pattern : securityCollectionDefinition.getPatterns()) {
|
|
||||||
|
|
||||||
/* the following code is asymmetric as Jetty's ConstraintMapping accepts only one allowed HTTP method,
|
|
||||||
but multiple omitted methods. Therefore we add one ConstraintMapping for each allowed
|
|
||||||
mapping but only one mapping in the cases of omitted methods or no methods.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (securityCollectionDefinition.getMethods().size() > 0) {
|
|
||||||
// according to the servlet spec we have either methods ...
|
|
||||||
for(String method : securityCollectionDefinition.getMethods()) {
|
|
||||||
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
|
||||||
jettyConstraintMappings.add(jettyConstraintMapping);
|
|
||||||
|
|
||||||
jettyConstraintMapping.setConstraint(jettyConstraint);
|
|
||||||
jettyConstraintMapping.setPathSpec(pattern);
|
|
||||||
jettyConstraintMapping.setMethod(method);
|
|
||||||
}
|
|
||||||
} else if (securityCollectionDefinition.getOmittedMethods().size() > 0){
|
|
||||||
// ... omitted methods ...
|
|
||||||
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
|
||||||
jettyConstraintMappings.add(jettyConstraintMapping);
|
|
||||||
|
|
||||||
jettyConstraintMapping.setConstraint(jettyConstraint);
|
|
||||||
jettyConstraintMapping.setPathSpec(pattern);
|
|
||||||
jettyConstraintMapping.setMethodOmissions(
|
|
||||||
securityCollectionDefinition.getOmittedMethods().toArray(new String[0]));
|
|
||||||
} else {
|
|
||||||
// ... or no methods at all
|
|
||||||
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
|
||||||
jettyConstraintMappings.add(jettyConstraintMapping);
|
|
||||||
|
|
||||||
jettyConstraintMapping.setConstraint(jettyConstraint);
|
|
||||||
jettyConstraintMapping.setPathSpec(pattern);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WebAppContext webAppContext = server.getBean(WebAppContext.class);
|
|
||||||
//if not found as registered bean let's try the handler
|
|
||||||
if(webAppContext==null){
|
|
||||||
webAppContext = (WebAppContext) server.getHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
|
|
||||||
securityHandler.setConstraintMappings(jettyConstraintMappings);
|
|
||||||
securityHandler.setAuthenticator(keycloakJettyAuthenticator);
|
|
||||||
|
|
||||||
webAppContext.setSecurityHandler(securityHandler);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static class KeycloakTomcatContextCustomizer implements TomcatContextCustomizer {
|
static class KeycloakTomcatContextCustomizer extends KeycloakBaseTomcatContextCustomizer implements TomcatContextCustomizer {
|
||||||
|
|
||||||
private final KeycloakSpringBootProperties keycloakProperties;
|
|
||||||
|
|
||||||
public KeycloakTomcatContextCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
public KeycloakTomcatContextCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
this.keycloakProperties = keycloakProperties;
|
super(keycloakProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void customize(Context context) {
|
|
||||||
LoginConfig loginConfig = new LoginConfig();
|
|
||||||
loginConfig.setAuthMethod("KEYCLOAK");
|
|
||||||
context.setLoginConfig(loginConfig);
|
|
||||||
|
|
||||||
Set<String> authRoles = new HashSet<String>();
|
|
||||||
for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) {
|
|
||||||
for (String authRole : constraint.getAuthRoles()) {
|
|
||||||
if (!authRoles.contains(authRole)) {
|
|
||||||
context.addSecurityRole(authRole);
|
|
||||||
authRoles.add(authRole);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) {
|
|
||||||
SecurityConstraint tomcatConstraint = new SecurityConstraint();
|
|
||||||
|
|
||||||
for (String authRole : constraint.getAuthRoles()) {
|
|
||||||
tomcatConstraint.addAuthRole(authRole);
|
|
||||||
if(authRole.equals("*") || authRole.equals("**")) {
|
|
||||||
// For some reasons embed tomcat don't set the auth constraint on true when wildcard is
|
|
||||||
// used
|
|
||||||
tomcatConstraint.setAuthConstraint(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (KeycloakSpringBootProperties.SecurityCollection collection : constraint.getSecurityCollections()) {
|
|
||||||
SecurityCollection tomcatSecCollection = new SecurityCollection();
|
|
||||||
|
|
||||||
if (collection.getName() != null) {
|
|
||||||
tomcatSecCollection.setName(collection.getName());
|
|
||||||
}
|
|
||||||
if (collection.getDescription() != null) {
|
|
||||||
tomcatSecCollection.setDescription(collection.getDescription());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String pattern : collection.getPatterns()) {
|
|
||||||
tomcatSecCollection.addPattern(pattern);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String method : collection.getMethods()) {
|
|
||||||
tomcatSecCollection.addMethod(method);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String method : collection.getOmittedMethods()) {
|
|
||||||
tomcatSecCollection.addOmittedMethod(method);
|
|
||||||
}
|
|
||||||
|
|
||||||
tomcatConstraint.addCollection(tomcatSecCollection);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.addConstraint(tomcatConstraint);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.addParameter("keycloak.config.resolver", KeycloakSpringBootConfigResolver.class.getName());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Executable
+151
@@ -0,0 +1,151 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-parent</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>4.0.0.Final</version>
|
||||||
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-spring-boot-2-adapter</artifactId>
|
||||||
|
<name>Keycloak Spring Boot 2 Integration</name>
|
||||||
|
<description/>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<spring-boot.version>2.0.0.RELEASE</spring-boot.version>
|
||||||
|
<spring.version>5.0.2.RELEASE</spring.version>
|
||||||
|
<mockito.version>1.9.5</mockito.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-spring-boot-adapter-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.logging</groupId>
|
||||||
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>spring-boot-container-bundle</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<optional>true</optional>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-spring-security-adapter</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.9.4</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-annotations</artifactId>
|
||||||
|
<version>2.9.4</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-core</artifactId>
|
||||||
|
<version>5.0.2.RELEASE</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.undertow</groupId>
|
||||||
|
<artifactId>undertow-servlet</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-server</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-security</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-webapp</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-test</artifactId>
|
||||||
|
<version>${spring.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-all</artifactId>
|
||||||
|
<version>${mockito.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.springboot;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||||
|
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
|
||||||
|
import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer;
|
||||||
|
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
|
||||||
|
import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer;
|
||||||
|
import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
|
||||||
|
import org.springframework.boot.web.embedded.undertow.UndertowDeploymentInfoCustomizer;
|
||||||
|
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keycloak authentication integration for Spring Boot 2
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConditionalOnWebApplication
|
||||||
|
@EnableConfigurationProperties(KeycloakSpringBootProperties.class)
|
||||||
|
@ConditionalOnProperty(value = "keycloak.enabled", matchIfMissing = true)
|
||||||
|
public class KeycloakAutoConfiguration extends KeycloakBaseSpringBootConfiguration {
|
||||||
|
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> getKeycloakContainerCustomizer() {
|
||||||
|
return new WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>() {
|
||||||
|
@Override
|
||||||
|
public void customize(ConfigurableServletWebServerFactory configurableServletWebServerFactory) {
|
||||||
|
if(configurableServletWebServerFactory instanceof TomcatServletWebServerFactory){
|
||||||
|
|
||||||
|
TomcatServletWebServerFactory container = (TomcatServletWebServerFactory)configurableServletWebServerFactory;
|
||||||
|
container.addContextValves(new KeycloakAuthenticatorValve());
|
||||||
|
container.addContextCustomizers(tomcatKeycloakContextCustomizer());
|
||||||
|
|
||||||
|
} else if (configurableServletWebServerFactory instanceof UndertowServletWebServerFactory){
|
||||||
|
|
||||||
|
UndertowServletWebServerFactory container = (UndertowServletWebServerFactory)configurableServletWebServerFactory;
|
||||||
|
container.addDeploymentInfoCustomizers(undertowKeycloakContextCustomizer());
|
||||||
|
|
||||||
|
} else if (configurableServletWebServerFactory instanceof JettyServletWebServerFactory){
|
||||||
|
|
||||||
|
JettyServletWebServerFactory container = (JettyServletWebServerFactory)configurableServletWebServerFactory;
|
||||||
|
container.addServerCustomizers(jettyKeycloakServerCustomizer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnClass(name = {"org.eclipse.jetty.webapp.WebAppContext"})
|
||||||
|
public JettyServerCustomizer jettyKeycloakServerCustomizer() {
|
||||||
|
return new KeycloakJettyServerCustomizer(keycloakProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnClass(name = {"org.apache.catalina.startup.Tomcat"})
|
||||||
|
public TomcatContextCustomizer tomcatKeycloakContextCustomizer() {
|
||||||
|
return new KeycloakTomcatContextCustomizer(keycloakProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnClass(name = {"io.undertow.Undertow"})
|
||||||
|
public UndertowDeploymentInfoCustomizer undertowKeycloakContextCustomizer() {
|
||||||
|
return new KeycloakUndertowDeploymentInfoCustomizer(keycloakProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
static class KeycloakJettyServerCustomizer extends KeycloakBaseJettyServerCustomizer implements JettyServerCustomizer {
|
||||||
|
|
||||||
|
|
||||||
|
public KeycloakJettyServerCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
|
super(keycloakProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class KeycloakTomcatContextCustomizer extends KeycloakBaseTomcatContextCustomizer implements TomcatContextCustomizer {
|
||||||
|
|
||||||
|
public KeycloakTomcatContextCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
|
super(keycloakProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class KeycloakUndertowDeploymentInfoCustomizer extends KeycloakBaseUndertowDeploymentInfoCustomizer implements UndertowDeploymentInfoCustomizer {
|
||||||
|
|
||||||
|
public KeycloakUndertowDeploymentInfoCustomizer(KeycloakSpringBootProperties keycloakProperties){
|
||||||
|
super(keycloakProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package org.keycloak.adapters.springboot.client;
|
||||||
|
|
||||||
|
import org.springframework.boot.web.client.RestTemplateCustomizer;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
public class KeycloakRestTemplateCustomizer implements RestTemplateCustomizer {
|
||||||
|
|
||||||
|
private final KeycloakSecurityContextClientRequestInterceptor keycloakInterceptor;
|
||||||
|
|
||||||
|
public KeycloakRestTemplateCustomizer() {
|
||||||
|
this(new KeycloakSecurityContextClientRequestInterceptor());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected KeycloakRestTemplateCustomizer(
|
||||||
|
KeycloakSecurityContextClientRequestInterceptor keycloakInterceptor
|
||||||
|
) {
|
||||||
|
this.keycloakInterceptor = keycloakInterceptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void customize(RestTemplate restTemplate) {
|
||||||
|
restTemplate.getInterceptors().add(keycloakInterceptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
package org.keycloak.adapters.springboot.client;
|
||||||
|
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.springframework.http.HttpRequest;
|
||||||
|
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||||
|
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||||
|
import org.springframework.http.client.ClientHttpResponse;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.Principal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interceptor for {@link ClientHttpRequestExecution} objects created for server to server secured
|
||||||
|
* communication using OAuth2 bearer tokens issued by Keycloak.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:jmcshan1@gmail.com">James McShane</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class KeycloakSecurityContextClientRequestInterceptor implements ClientHttpRequestInterceptor {
|
||||||
|
|
||||||
|
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link KeycloakSecurityContext} from the Spring {@link ServletRequestAttributes}'s {@link Principal}.
|
||||||
|
*
|
||||||
|
* The principal must support retrieval of the KeycloakSecurityContext, so at this point, only {@link KeycloakPrincipal}
|
||||||
|
* values are supported
|
||||||
|
*
|
||||||
|
* @return the current <code>KeycloakSecurityContext</code>
|
||||||
|
*/
|
||||||
|
protected KeycloakSecurityContext getKeycloakSecurityContext() {
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
Principal principal = attributes.getRequest().getUserPrincipal();
|
||||||
|
if (principal == null) {
|
||||||
|
throw new IllegalStateException("Cannot set authorization header because there is no authenticated principal");
|
||||||
|
}
|
||||||
|
if (!(principal instanceof KeycloakPrincipal)) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
String.format(
|
||||||
|
"Cannot set authorization header because the principal type %s does not provide the KeycloakSecurityContext",
|
||||||
|
principal.getClass()));
|
||||||
|
}
|
||||||
|
return ((KeycloakPrincipal) principal).getKeycloakSecurityContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
|
||||||
|
KeycloakSecurityContext context = this.getKeycloakSecurityContext();
|
||||||
|
httpRequest.getHeaders().set(AUTHORIZATION_HEADER, "Bearer " + context.getTokenString());
|
||||||
|
return clientHttpRequestExecution.execute(httpRequest, bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||||
|
org.keycloak.adapters.springboot.KeycloakAutoConfiguration
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package org.keycloak.adapters.springboot.client;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
public class KeycloakRestTemplateCustomizerTest {
|
||||||
|
|
||||||
|
private KeycloakRestTemplateCustomizer customizer;
|
||||||
|
private KeycloakSecurityContextClientRequestInterceptor interceptor =
|
||||||
|
mock(KeycloakSecurityContextClientRequestInterceptor.class);
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
customizer = new KeycloakRestTemplateCustomizer(interceptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void interceptorIsAddedToRequest() {
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
customizer.customize(restTemplate);
|
||||||
|
assertTrue(restTemplate.getInterceptors().contains(interceptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.springboot.client;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.mockito.Spy;
|
||||||
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keycloak spring boot client request factory tests.
|
||||||
|
*/
|
||||||
|
public class KeycloakSecurityContextClientRequestInterceptorTest {
|
||||||
|
|
||||||
|
@Spy
|
||||||
|
private KeycloakSecurityContextClientRequestInterceptor factory;
|
||||||
|
|
||||||
|
private MockHttpServletRequest servletRequest;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private KeycloakSecurityContext keycloakSecurityContext;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private KeycloakPrincipal keycloakPrincipal;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this);
|
||||||
|
servletRequest = new MockHttpServletRequest();
|
||||||
|
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(servletRequest));
|
||||||
|
servletRequest.setUserPrincipal(keycloakPrincipal);
|
||||||
|
when(keycloakPrincipal.getKeycloakSecurityContext()).thenReturn(keycloakSecurityContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetKeycloakSecurityContext() throws Exception {
|
||||||
|
KeycloakSecurityContext context = factory.getKeycloakSecurityContext();
|
||||||
|
assertNotNull(context);
|
||||||
|
assertEquals(keycloakSecurityContext, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalStateException.class)
|
||||||
|
public void testGetKeycloakSecurityContextInvalidPrincipal() throws Exception {
|
||||||
|
servletRequest.setUserPrincipal(new MarkerPrincipal());
|
||||||
|
factory.getKeycloakSecurityContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalStateException.class)
|
||||||
|
public void testGetKeycloakSecurityContextNullAuthentication() throws Exception {
|
||||||
|
servletRequest.setUserPrincipal(null);
|
||||||
|
factory.getKeycloakSecurityContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MarkerPrincipal implements Principal {
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>3.4.3.Final</version>
|
<version>4.0.0.Final</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+21
-1
@@ -24,6 +24,8 @@ import org.keycloak.adapters.spi.LogoutError;
|
|||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -40,6 +42,7 @@ import java.util.List;
|
|||||||
class WrappedHttpServletRequest implements Request {
|
class WrappedHttpServletRequest implements Request {
|
||||||
|
|
||||||
private final HttpServletRequest request;
|
private final HttpServletRequest request;
|
||||||
|
private InputStream inputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new request for the given <code>HttpServletRequest</code>
|
* Creates a new request for the given <code>HttpServletRequest</code>
|
||||||
@@ -122,10 +125,27 @@ class WrappedHttpServletRequest implements Request {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream getInputStream() {
|
public InputStream getInputStream() {
|
||||||
|
return getInputStream(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream(boolean buffered) {
|
||||||
|
if (inputStream != null) {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffered) {
|
||||||
|
try {
|
||||||
|
return inputStream = new BufferedInputStream(request.getInputStream());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return request.getInputStream();
|
return request.getInputStream();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("Unable to get request input stream", e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loaded 100 of 4596 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user