Finish up the first iterator of keycloak integration; it now does login!
Showing
10 changed files
with
274 additions
and
104 deletions
... | @@ -9,4 +9,13 @@ | ... | @@ -9,4 +9,13 @@ |
9 | <tools> | 9 | <tools> |
10 | <tool-factory class="org.moqui.keycloak.KeycloakToolFactory" init-priority="20" disabled="false"/> | 10 | <tool-factory class="org.moqui.keycloak.KeycloakToolFactory" init-priority="20" disabled="false"/> |
11 | </tools> | 11 | </tools> |
12 | <webapp-list> | ||
13 | <webapp name="webroot"> | ||
14 | <before-logout> | ||
15 | <actions> | ||
16 | <script location="component://moqui-keycloak/scripts/BeforeLogout.groovy"/> | ||
17 | </actions> | ||
18 | </before-logout> | ||
19 | </webapp> | ||
20 | </webapp-list> | ||
12 | </moqui-conf> | 21 | </moqui-conf> | ... | ... |
config/moqui-keycloak.json
0 → 100644
1 | { | ||
2 | "principal-attribute": "sub", | ||
3 | "enable-cors": true, | ||
4 | |||
5 | "realm": "${env.moqui_keycloak_realm}", | ||
6 | "disable-trust-manager": true, | ||
7 | "auth-server-url": "${env.moqui_keycloak_server_url}", | ||
8 | "ssl-required": "external", | ||
9 | "resource": "${env.moqui_keycloak_client_id}", | ||
10 | "confidential-port": 0, | ||
11 | "use-resource-role-mappings": true, | ||
12 | "autodetect-bearer-only": false, | ||
13 | "bearer-only": false, | ||
14 | "verify-token-audience": true, | ||
15 | "credentials": { | ||
16 | "secret": "${env.moqui_keycloak_client_secret}" | ||
17 | } | ||
18 | } | ||
19 |
1 | <screen-extend xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/xml-screen-3.xsd"> | ||
2 | |||
3 | <form-single name="UpdateUserAccount"> | ||
4 | <field name="externalUserId"><default-field><text-line size="60"/></default-field></field> | ||
5 | </form-single> | ||
6 | |||
7 | </screen-extend> |
1 | <screen-extend xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/xml-screen-3.xsd"> | ||
2 | |||
3 | <actions-extend type="pre-actions" when="before"> | ||
4 | <script location="component://moqui-keycloak/scripts/LoginPreActions.groovy"/> | ||
5 | </actions-extend> | ||
6 | </screen-extend> |
1 | <screen-extend xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/xml-screen-3.xsd"> | ||
2 | |||
3 | <actions-extend type="always-actions"> | ||
4 | <script location="component://moqui-keycloak/scripts/RestPreActions.groovy"/> | ||
5 | </actions-extend> | ||
6 | <!-- | ||
7 | |||
8 | <actions-extend type="pre-actions" when="before"> | ||
9 | <script location="component://moqui-keycloak/scripts/LoginPreActions.groovy"/> | ||
10 | </actions-extend> | ||
11 | <section name="NeedsSetupSection"> | ||
12 | <widgets> | ||
13 | <render-mode><text type="html"><![CDATA[<h1>Keycloak!</h1>]]></text></render-mode> | ||
14 | </widgets> | ||
15 | </section> | ||
16 | |||
17 | --> | ||
18 | </screen-extend> |
scripts/BeforeLogout.groovy
0 → 100644
1 | import org.keycloak.adapters.KeycloakDeploymentBuilder | ||
2 | import org.keycloak.adapters.servlet.OIDCServletHttpFacade | ||
3 | import org.keycloak.adapters.servlet.OIDCFilterSessionStore | ||
4 | |||
5 | ec.logger.info("moqui-keycloak:before-logout") | ||
6 | |||
7 | |||
8 | def cache = ec.getCache().getCache('moqui-keycloak:deployment') | ||
9 | |||
10 | def keycloakJsonUrl = 'component://moqui-keycloak/config/moqui-keycloak.json' | ||
11 | def configStream = ec.getResource().getLocationStream(keycloakJsonUrl) | ||
12 | def keycloakDeployment = KeycloakDeploymentBuilder.build(configStream) | ||
13 | |||
14 | def idMapper = cache.get('SessionIdMapper') | ||
15 | if (idMapper == null) return | ||
16 | |||
17 | def facade = new OIDCServletHttpFacade(ec.web.request, ec.web.response) | ||
18 | def tokenStore = new OIDCFilterSessionStore(ec.web.request, facade, 100000, keycloakDeployment, idMapper) | ||
19 | |||
20 | tokenStore.logout() |
scripts/LoginPreActions.groovy
0 → 100644
1 | import javax.servlet.http.HttpServletRequestWrapper | ||
2 | |||
3 | import org.keycloak.KeycloakSecurityContext | ||
4 | import org.keycloak.AuthorizationContext | ||
5 | |||
6 | import org.keycloak.representations.AccessToken | ||
7 | import org.keycloak.representations.IDToken | ||
8 | import org.keycloak.adapters.KeycloakDeployment | ||
9 | import org.keycloak.adapters.KeycloakDeploymentBuilder | ||
10 | |||
11 | import org.keycloak.adapters.AdapterDeploymentContext | ||
12 | import org.keycloak.adapters.AuthenticatedActionsHandler | ||
13 | import org.keycloak.adapters.KeycloakConfigResolver | ||
14 | import org.keycloak.adapters.NodesRegistrationManagement | ||
15 | import org.keycloak.adapters.PreAuthActionsHandler | ||
16 | import org.keycloak.adapters.spi.AuthChallenge | ||
17 | import org.keycloak.adapters.spi.AuthOutcome | ||
18 | import org.keycloak.adapters.spi.InMemorySessionIdMapper | ||
19 | import org.keycloak.adapters.spi.SessionIdMapper | ||
20 | import org.keycloak.adapters.spi.UserSessionManagement | ||
21 | |||
22 | import org.keycloak.adapters.servlet.OIDCServletHttpFacade | ||
23 | import org.keycloak.adapters.servlet.OIDCFilterSessionStore | ||
24 | import org.keycloak.adapters.servlet.FilterRequestAuthenticator | ||
25 | |||
26 | def cache = ec.getCache().getCache('moqui-keycloak:deployment') | ||
27 | |||
28 | def keycloakJsonUrl = 'component://moqui-keycloak/config/moqui-keycloak.json' | ||
29 | def keycloakDeployment = cache.get(keycloakJsonUrl) | ||
30 | |||
31 | def idMapper = cache.get('SessionIdMapper') | ||
32 | if (idMapper == null) { | ||
33 | idMapper = new InMemorySessionIdMapper() | ||
34 | cache.put('SessionIdMapper', idMapper) | ||
35 | idMapper = cache.get('SessionIdMapper') | ||
36 | } | ||
37 | |||
38 | if (true || !keycloakDeployment) { | ||
39 | def configStream = ec.getResource().getLocationStream(keycloakJsonUrl) | ||
40 | keycloakDeployment = KeycloakDeploymentBuilder.build(configStream) | ||
41 | cache.put(keycloakJsonUrl, keycloakDeployment) | ||
42 | } | ||
43 | def facade = new OIDCServletHttpFacade(ec.web.request, ec.web.response) | ||
44 | def tokenStore = new OIDCFilterSessionStore(ec.web.request, facade, 100000, keycloakDeployment, idMapper) | ||
45 | |||
46 | def authenticator = new FilterRequestAuthenticator(keycloakDeployment, tokenStore, facade, ec.web.request, 8443) | ||
47 | def outcome = authenticator.authenticate() | ||
48 | ec.logger.info("outcome2: ${outcome}") | ||
49 | if (outcome == AuthOutcome.AUTHENTICATED) { | ||
50 | if (facade.isEnded()) { | ||
51 | ec.logger.info('facade has ended') | ||
52 | } else { | ||
53 | def actions = new AuthenticatedActionsHandler(keycloakDeployment, facade) | ||
54 | ec.logger.info("actions: ${actions}") | ||
55 | if (actions.handledRequest()) { | ||
56 | ec.logger.info("actions.handledRequest") | ||
57 | new Exception().printStackTrace() | ||
58 | //sri.stopRender() | ||
59 | return | ||
60 | } else { | ||
61 | //HttpServletRequestWrapper wrapper = tokenStore.buildWrapper() | ||
62 | //postKeycloakFilter(wrapper, response, chain) | ||
63 | //return | ||
64 | } | ||
65 | } | ||
66 | } else { | ||
67 | AuthChallenge challenge = authenticator.getChallenge() | ||
68 | ec.logger.info("challenge: ${challenge}") | ||
69 | if (challenge != null) { | ||
70 | if (challenge.challenge(facade)) { | ||
71 | ec.logger.info("challenge sent") | ||
72 | new Exception().printStackTrace() | ||
73 | //sri.stopRender() | ||
74 | return | ||
75 | } | ||
76 | } | ||
77 | } | ||
78 | |||
79 | def ksc = ec.web.request.getAttribute(KeycloakSecurityContext.class.getName()) | ||
80 | def idToken = ksc.getIdToken() | ||
81 | ec.logger.info("idToken: ${idToken}") | ||
82 | def subject = idToken.getSubject() | ||
83 | ec.logger.info("subject: ${subject}") | ||
84 | |||
85 | def accessToken = ksc.getToken() | ||
86 | ec.logger.info("accessToken: ${accessToken}") | ||
87 | |||
88 | EntityValue userAccount = ec.entity.find('UserAccount').condition('externalUserId', subject).disableAuthz().one() | ||
89 | ec.logger.info("userAccount: ${userAccount}") | ||
90 | if (userAccount) { | ||
91 | //ec.user.pushUser(userAccount.username) | ||
92 | ec.user.internalLoginUser(userAccount.username) | ||
93 | } | ||
94 |
scripts/RestPreActions.groovy
0 → 100644
1 | import javax.servlet.http.HttpServletRequestWrapper | ||
2 | |||
3 | import org.keycloak.KeycloakSecurityContext | ||
4 | import org.keycloak.AuthorizationContext | ||
5 | |||
6 | import org.keycloak.representations.AccessToken | ||
7 | import org.keycloak.representations.IDToken | ||
8 | import org.keycloak.adapters.KeycloakDeployment | ||
9 | import org.keycloak.adapters.KeycloakDeploymentBuilder | ||
10 | |||
11 | import org.keycloak.adapters.AdapterDeploymentContext | ||
12 | import org.keycloak.adapters.AuthenticatedActionsHandler | ||
13 | import org.keycloak.adapters.KeycloakConfigResolver | ||
14 | import org.keycloak.adapters.NodesRegistrationManagement | ||
15 | import org.keycloak.adapters.PreAuthActionsHandler | ||
16 | import org.keycloak.adapters.spi.AuthChallenge | ||
17 | import org.keycloak.adapters.spi.AuthOutcome | ||
18 | import org.keycloak.adapters.spi.InMemorySessionIdMapper | ||
19 | import org.keycloak.adapters.spi.SessionIdMapper | ||
20 | import org.keycloak.adapters.spi.UserSessionManagement | ||
21 | |||
22 | import org.keycloak.adapters.servlet.OIDCServletHttpFacade | ||
23 | import org.keycloak.adapters.servlet.OIDCFilterSessionStore | ||
24 | import org.keycloak.adapters.servlet.FilterRequestAuthenticator | ||
25 | |||
26 | def cache = ec.getCache().getCache('moqui-keycloak:deployment') | ||
27 | |||
28 | ec.logger.info('rest:pre-actions') | ||
29 | def keycloakJsonUrl = 'component://moqui-keycloak/config/moqui-keycloak.json' | ||
30 | def keycloakDeployment = cache.get(keycloakJsonUrl) | ||
31 | |||
32 | def idMapper = cache.get('SessionIdMapper') | ||
33 | if (idMapper == null) { | ||
34 | idMapper = new InMemorySessionIdMapper() | ||
35 | cache.put('SessionIdMapper', idMapper) | ||
36 | idMapper = cache.get('SessionIdMapper') | ||
37 | } | ||
38 | |||
39 | if (true || !keycloakDeployment) { | ||
40 | def configStream = ec.getResource().getLocationStream(keycloakJsonUrl) | ||
41 | keycloakDeployment = KeycloakDeploymentBuilder.build(configStream) | ||
42 | cache.put(keycloakJsonUrl, keycloakDeployment) | ||
43 | } | ||
44 | def facade = new OIDCServletHttpFacade(ec.web.request, ec.web.response) | ||
45 | def tokenStore = new OIDCFilterSessionStore(ec.web.request, facade, 100000, keycloakDeployment, idMapper) | ||
46 | |||
47 | def authenticator = new FilterRequestAuthenticator(keycloakDeployment, tokenStore, facade, ec.web.request, 8443) | ||
48 | ec.logger.info("authenticator: ${authenticator}") | ||
49 | def outcome = authenticator.authenticate() | ||
50 | ec.logger.info("rest:outcome: ${outcome}") | ||
51 | ec.logger.info("request: ${ec.web.requestDetails}") | ||
52 | if (outcome == AuthOutcome.AUTHENTICATED) { | ||
53 | if (facade.isEnded()) { | ||
54 | ec.logger.info('facade has ended') | ||
55 | } else { | ||
56 | def actions = new AuthenticatedActionsHandler(keycloakDeployment, facade) | ||
57 | ec.logger.info("actions: ${actions}") | ||
58 | if (actions.handledRequest()) { | ||
59 | ec.logger.info("actions.handledRequest") | ||
60 | sri.stopRender() | ||
61 | return | ||
62 | } | ||
63 | } | ||
64 | } else { | ||
65 | AuthChallenge challenge = authenticator.getChallenge() | ||
66 | ec.logger.info("challenge: ${challenge}") | ||
67 | if (challenge != null) { | ||
68 | if (challenge.challenge(facade)) { | ||
69 | //sri.stopRender() | ||
70 | //return | ||
71 | } | ||
72 | } | ||
73 | } | ||
74 | |||
75 | def ksc = ec.web.request.getAttribute(KeycloakSecurityContext.class.getName()) | ||
76 | if (ksc) { | ||
77 | def idToken = ksc.getIdToken() | ||
78 | ec.logger.info("idToken: ${idToken}") | ||
79 | if (idToken) { | ||
80 | def subject = idToken.getSubject() | ||
81 | ec.logger.info("subject: ${subject}") | ||
82 | } | ||
83 | def accessToken = ksc.getToken() | ||
84 | ec.logger.info("accessToken: ${accessToken}") | ||
85 | |||
86 | if (accessToken) { | ||
87 | def subject = accessToken.getSubject() | ||
88 | EntityValue userAccount = ec.entity.find('UserAccount').condition('externalUserId', subject).disableAuthz().one() | ||
89 | ec.logger.info("userAccount: ${userAccount}") | ||
90 | if (userAccount) { | ||
91 | //ec.user.pushUser(userAccount.username) | ||
92 | ec.user.internalLoginUser(userAccount.username) | ||
93 | } | ||
94 | } | ||
95 | } | ||
96 |
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 | } |
... | @@ -57,11 +57,12 @@ class KeycloakToolFactory implements ToolFactory<Keycloak> { | ... | @@ -57,11 +57,12 @@ class KeycloakToolFactory implements ToolFactory<Keycloak> { |
57 | 57 | ||
58 | @Override | 58 | @Override |
59 | void preFacadeInit(ExecutionContextFactory ecf) { | 59 | void preFacadeInit(ExecutionContextFactory ecf) { |
60 | logger.info("Configuring keycloakBuilder") | ||
60 | this.keycloakBuilder = KeycloakBuilder.builder() | 61 | this.keycloakBuilder = KeycloakBuilder.builder() |
61 | .serverUrl((String) System.getProperty("moqui_keycloak_server_url")) | 62 | .serverUrl(getServerUrl()) |
62 | .realm((String) System.getProperty("moqui_keycloak_realm")) | 63 | .realm(getRealmName()) |
63 | .grantType((String) OAuth2Constants.CLIENT_CREDENTIALS) | 64 | .grantType((String) OAuth2Constants.CLIENT_CREDENTIALS) |
64 | .clientId((String) System.getProperty("moqui_keycloak_client_id")) | 65 | .clientId(getClientId()) |
65 | .clientSecret((String) System.getProperty("moqui_keycloak_client_secret")) | 66 | .clientSecret((String) System.getProperty("moqui_keycloak_client_secret")) |
66 | } | 67 | } |
67 | 68 | ||
... | @@ -70,10 +71,18 @@ class KeycloakToolFactory implements ToolFactory<Keycloak> { | ... | @@ -70,10 +71,18 @@ class KeycloakToolFactory implements ToolFactory<Keycloak> { |
70 | return keycloakBuilder.build() | 71 | return keycloakBuilder.build() |
71 | } | 72 | } |
72 | 73 | ||
74 | String getClientId() { | ||
75 | return (String) System.getProperty("moqui_keycloak_client_id") | ||
76 | } | ||
77 | |||
73 | String getRealmName() { | 78 | String getRealmName() { |
74 | return (String) System.getProperty("moqui_keycloak_realm") | 79 | return (String) System.getProperty("moqui_keycloak_realm") |
75 | } | 80 | } |
76 | 81 | ||
82 | String getServerUrl() { | ||
83 | return (String) System.getProperty("moqui_keycloak_server_url") | ||
84 | } | ||
85 | |||
77 | @Override | 86 | @Override |
78 | void destroy() { | 87 | void destroy() { |
79 | } | 88 | } | ... | ... |
-
Please register or sign in to post a comment