93137dec by Adam Heath

Finish up the first iterator of keycloak integration; it now does login!

1 parent 61fe5e33
...@@ -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>
......
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>
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()
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
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 }
......