2065117c by Adam Heath

Initial pass of a keycloak filter; it compiles, but hasn't been loaded

into moqui yet.
1 parent f12184d3
1 /*
2 * This software is in the public domain under CC0 1.0 Universal plus a
3 * Grant of Patent License.
4 *
5 * To the extent possible under law, the author(s) have dedicated all
6 * copyright and related and neighboring rights to this software to the
7 * public domain worldwide. This software is distributed without any
8 * warranty.
9 *
10 * You should have received a copy of the CC0 Public Domain Dedication
11 * along with this software (see the LICENSE.md file). If not, see
12 * <http://creativecommons.org/publicdomain/zero/1.0/>.
13 */
14
15 apply plugin: 'groovy'
16 apply plugin: 'java-library'
17
18 sourceCompatibility = '1.11'
19 targetCompatibility = '1.11'
20 def moquiDir = projectDir.parentFile.parentFile.parentFile
21 def frameworkDir = file(moquiDir.absolutePath + '/framework')
22 def componentNode = parseComponent(project)
23 version = componentNode.'@version'
24
25 // maybe in the future: repositories { mavenCentral() }
26 repositories {
27 flatDir name: 'localLib', dirs: frameworkDir.absolutePath + '/lib'
28 flatDir name: 'librepo', dirs: projectDir.absolutePath + '/librepo'
29 mavenCentral()
30 }
31
32 // Log4J has annotation processors, disable to avoid warning
33 tasks.withType(JavaCompile) { options.compilerArgs << "-proc:none" }
34 tasks.withType(GroovyCompile) { options.compilerArgs << "-proc:none" }
35
36 dependencies {
37 implementation project(':framework')
38 implementation 'org.keycloak:keycloak-servlet-filter-adapter:15.0.2'
39 }
40
41 task cleanLib(type: Delete) { delete fileTree(dir: projectDir.absolutePath+'/lib', include: '*') }
42
43 // by default the Java plugin runs test on build, change to not do that (only run test if explicit task)
44 // no longer workds as of gradle 4.8 or possibly earlier, use clear() instead: check.dependsOn.remove(test)
45 check.dependsOn.clear()
46
47 jar {
48 destinationDir = file(projectDir.absolutePath + '/lib')
49 baseName = componentNode.'@name'
50 }
51
52 task copyDependencies { doLast {
53 copy { from (configurations.runtimeClasspath - project(':framework').configurations.runtimeClasspath - project(':framework').jar.archivePath)
54 into file(projectDir.absolutePath + '/lib') }
55 } }
56 copyDependencies.dependsOn cleanLib
57 jar.dependsOn copyDependencies
58
1 <?xml version="1.0" encoding="UTF-8"?>
2 <component xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/moqui-conf-3.xsd"
3 name="moqui-keycloak" version="0.0.0">
4 <depends-on name="mantle-udm" version="2.2.0"/>
5 </component>
1 package org.moqui.keycloak;
2
3 import javax.servlet.Filter;
4 import javax.servlet.FilterChain;
5 import javax.servlet.FilterConfig;
6 import javax.servlet.ServletException;
7 import javax.servlet.ServletRequest;
8 import javax.servlet.ServletResponse;
9 import javax.servlet.http.HttpServletRequest;
10 import javax.servlet.http.HttpServletRequestWrapper;
11 import javax.servlet.http.HttpServletResponse;
12 import javax.servlet.http.HttpSession;
13
14 public class MoquiKeycloakSecurityFilter implements Filter {
15
16 @Override
17 public void init(FilterConfig config) throws ServletException {
18 }
19
20 @Override
21 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
22 if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
23 doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
24 } else {
25 chain.doFilter(request, response);
26 }
27 }
28
29 public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
30 OIDCServletHttpFacade facade = new OIDCServletHttpFacade(request, response)
31 KeycloakDeployment keycloakDeployment = getKeycloakDeployment()
32 OIDCFilterSessionStore tokenStore = new OIDCFilterSessionStore(request, facade, 100000, keycloakDeployment, idMapper)
33 // TODO: Look at the PolicyEnforcer stuff in keycloak, perhaps hook into that for moqui
34 // if the thing being called doesn't require AUTH, then the enforcer should return AUTHENTICATED?
35 FilterRequestAuthenticator authenticator = new FilterRequestAuthenticator(keycloakDeployment, tokenStore, facade, request, 8443)
36 AuthOutcome outcome = authenticator.authenticate()
37 if (outcome == AuthOutcome.AUTHENTICATED) {
38 if (facade.isEnded()) {
39 return;
40 }
41 AuthenticatedActionsHandler actions = new AuthenticatedActionsHandler(keycloakDeployment, facade);
42 if (actions.handledRequest()) {
43 return;
44 } else {
45 HttpServletRequestWrapper wrapper = tokenStore.buildWrapper();
46 postKeycloakFilter(wrapper, response, chain);
47 return;
48 }
49 }
50 AuthChallenge challenge = authenticator.getChallenge();
51 if (challenge != null) {
52 if (request.getRequestURI().equals('/Login')) {
53 challenge.challenge(facade);
54 return;
55 }
56 if (request.getMethod().equals('GET')) {
57 challenge.challenge(facade);
58 return;
59 }
60 // TODO
61 //challenge.challenge(facade);
62 //return;
63 }
64 // TODO
65 // sendError(403)
66 postKeycloakFilter(request, response, chain);
67 }
68
69 protected void postKeycloakFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
70 // Can also look at the session if needed
71 KeycloakSecurityContext ksc = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
72 logger.debug("doFilter(" + ksc + ")");
73 //showKeycloakSecurityContext(ksc);
74 //importKeycloakSecurityContext(ksc);
75 String username = null;
76 if (ksc != null) {
77 ExecutionContext ec = Moqui.getExecutionContext();
78 //ec.user.pushUser('keycloak-api');
79 try {
80 // FIXME: This is bad, I don't know how to force a login without a password.
81 ec.getUser().loginUser('keycloak-api', 'moqui');
82 Map<String, Object> result = ec.service.sync().name("bf.auth.KeycloakServices.import#KeycloakUser").parameters([ksc: ksc]).call();
83 logger.debug('result=' + result)
84 username = result?.userAccount?.username
85 request.setAttribute('moqui.request.authenticated', 'true')
86 } finally {
87 //ec.user.popUser();
88 }
89 }
90 shiroRunAs(username, request.getSession(), { chain.doFilter(request, response) });
91 }
92
93
94 }