93137dec by Adam Heath

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

1 parent 61fe5e33
......@@ -9,4 +9,13 @@
<tools>
<tool-factory class="org.moqui.keycloak.KeycloakToolFactory" init-priority="20" disabled="false"/>
</tools>
<webapp-list>
<webapp name="webroot">
<before-logout>
<actions>
<script location="component://moqui-keycloak/scripts/BeforeLogout.groovy"/>
</actions>
</before-logout>
</webapp>
</webapp-list>
</moqui-conf>
......
{
"principal-attribute": "sub",
"enable-cors": true,
"realm": "${env.moqui_keycloak_realm}",
"disable-trust-manager": true,
"auth-server-url": "${env.moqui_keycloak_server_url}",
"ssl-required": "external",
"resource": "${env.moqui_keycloak_client_id}",
"confidential-port": 0,
"use-resource-role-mappings": true,
"autodetect-bearer-only": false,
"bearer-only": false,
"verify-token-audience": true,
"credentials": {
"secret": "${env.moqui_keycloak_client_secret}"
}
}
<screen-extend xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/xml-screen-3.xsd">
<form-single name="UpdateUserAccount">
<field name="externalUserId"><default-field><text-line size="60"/></default-field></field>
</form-single>
</screen-extend>
<screen-extend xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/xml-screen-3.xsd">
<actions-extend type="pre-actions" when="before">
<script location="component://moqui-keycloak/scripts/LoginPreActions.groovy"/>
</actions-extend>
</screen-extend>
<screen-extend xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/xml-screen-3.xsd">
<actions-extend type="always-actions">
<script location="component://moqui-keycloak/scripts/RestPreActions.groovy"/>
</actions-extend>
<!--
<actions-extend type="pre-actions" when="before">
<script location="component://moqui-keycloak/scripts/LoginPreActions.groovy"/>
</actions-extend>
<section name="NeedsSetupSection">
<widgets>
<render-mode><text type="html"><![CDATA[<h1>Keycloak!</h1>]]></text></render-mode>
</widgets>
</section>
-->
</screen-extend>
import org.keycloak.adapters.KeycloakDeploymentBuilder
import org.keycloak.adapters.servlet.OIDCServletHttpFacade
import org.keycloak.adapters.servlet.OIDCFilterSessionStore
ec.logger.info("moqui-keycloak:before-logout")
def cache = ec.getCache().getCache('moqui-keycloak:deployment')
def keycloakJsonUrl = 'component://moqui-keycloak/config/moqui-keycloak.json'
def configStream = ec.getResource().getLocationStream(keycloakJsonUrl)
def keycloakDeployment = KeycloakDeploymentBuilder.build(configStream)
def idMapper = cache.get('SessionIdMapper')
if (idMapper == null) return
def facade = new OIDCServletHttpFacade(ec.web.request, ec.web.response)
def tokenStore = new OIDCFilterSessionStore(ec.web.request, facade, 100000, keycloakDeployment, idMapper)
tokenStore.logout()
import javax.servlet.http.HttpServletRequestWrapper
import org.keycloak.KeycloakSecurityContext
import org.keycloak.AuthorizationContext
import org.keycloak.representations.AccessToken
import org.keycloak.representations.IDToken
import org.keycloak.adapters.KeycloakDeployment
import org.keycloak.adapters.KeycloakDeploymentBuilder
import org.keycloak.adapters.AdapterDeploymentContext
import org.keycloak.adapters.AuthenticatedActionsHandler
import org.keycloak.adapters.KeycloakConfigResolver
import org.keycloak.adapters.NodesRegistrationManagement
import org.keycloak.adapters.PreAuthActionsHandler
import org.keycloak.adapters.spi.AuthChallenge
import org.keycloak.adapters.spi.AuthOutcome
import org.keycloak.adapters.spi.InMemorySessionIdMapper
import org.keycloak.adapters.spi.SessionIdMapper
import org.keycloak.adapters.spi.UserSessionManagement
import org.keycloak.adapters.servlet.OIDCServletHttpFacade
import org.keycloak.adapters.servlet.OIDCFilterSessionStore
import org.keycloak.adapters.servlet.FilterRequestAuthenticator
def cache = ec.getCache().getCache('moqui-keycloak:deployment')
def keycloakJsonUrl = 'component://moqui-keycloak/config/moqui-keycloak.json'
def keycloakDeployment = cache.get(keycloakJsonUrl)
def idMapper = cache.get('SessionIdMapper')
if (idMapper == null) {
idMapper = new InMemorySessionIdMapper()
cache.put('SessionIdMapper', idMapper)
idMapper = cache.get('SessionIdMapper')
}
if (true || !keycloakDeployment) {
def configStream = ec.getResource().getLocationStream(keycloakJsonUrl)
keycloakDeployment = KeycloakDeploymentBuilder.build(configStream)
cache.put(keycloakJsonUrl, keycloakDeployment)
}
def facade = new OIDCServletHttpFacade(ec.web.request, ec.web.response)
def tokenStore = new OIDCFilterSessionStore(ec.web.request, facade, 100000, keycloakDeployment, idMapper)
def authenticator = new FilterRequestAuthenticator(keycloakDeployment, tokenStore, facade, ec.web.request, 8443)
def outcome = authenticator.authenticate()
ec.logger.info("outcome2: ${outcome}")
if (outcome == AuthOutcome.AUTHENTICATED) {
if (facade.isEnded()) {
ec.logger.info('facade has ended')
} else {
def actions = new AuthenticatedActionsHandler(keycloakDeployment, facade)
ec.logger.info("actions: ${actions}")
if (actions.handledRequest()) {
ec.logger.info("actions.handledRequest")
new Exception().printStackTrace()
//sri.stopRender()
return
} else {
//HttpServletRequestWrapper wrapper = tokenStore.buildWrapper()
//postKeycloakFilter(wrapper, response, chain)
//return
}
}
} else {
AuthChallenge challenge = authenticator.getChallenge()
ec.logger.info("challenge: ${challenge}")
if (challenge != null) {
if (challenge.challenge(facade)) {
ec.logger.info("challenge sent")
new Exception().printStackTrace()
//sri.stopRender()
return
}
}
}
def ksc = ec.web.request.getAttribute(KeycloakSecurityContext.class.getName())
def idToken = ksc.getIdToken()
ec.logger.info("idToken: ${idToken}")
def subject = idToken.getSubject()
ec.logger.info("subject: ${subject}")
def accessToken = ksc.getToken()
ec.logger.info("accessToken: ${accessToken}")
EntityValue userAccount = ec.entity.find('UserAccount').condition('externalUserId', subject).disableAuthz().one()
ec.logger.info("userAccount: ${userAccount}")
if (userAccount) {
//ec.user.pushUser(userAccount.username)
ec.user.internalLoginUser(userAccount.username)
}
import javax.servlet.http.HttpServletRequestWrapper
import org.keycloak.KeycloakSecurityContext
import org.keycloak.AuthorizationContext
import org.keycloak.representations.AccessToken
import org.keycloak.representations.IDToken
import org.keycloak.adapters.KeycloakDeployment
import org.keycloak.adapters.KeycloakDeploymentBuilder
import org.keycloak.adapters.AdapterDeploymentContext
import org.keycloak.adapters.AuthenticatedActionsHandler
import org.keycloak.adapters.KeycloakConfigResolver
import org.keycloak.adapters.NodesRegistrationManagement
import org.keycloak.adapters.PreAuthActionsHandler
import org.keycloak.adapters.spi.AuthChallenge
import org.keycloak.adapters.spi.AuthOutcome
import org.keycloak.adapters.spi.InMemorySessionIdMapper
import org.keycloak.adapters.spi.SessionIdMapper
import org.keycloak.adapters.spi.UserSessionManagement
import org.keycloak.adapters.servlet.OIDCServletHttpFacade
import org.keycloak.adapters.servlet.OIDCFilterSessionStore
import org.keycloak.adapters.servlet.FilterRequestAuthenticator
def cache = ec.getCache().getCache('moqui-keycloak:deployment')
ec.logger.info('rest:pre-actions')
def keycloakJsonUrl = 'component://moqui-keycloak/config/moqui-keycloak.json'
def keycloakDeployment = cache.get(keycloakJsonUrl)
def idMapper = cache.get('SessionIdMapper')
if (idMapper == null) {
idMapper = new InMemorySessionIdMapper()
cache.put('SessionIdMapper', idMapper)
idMapper = cache.get('SessionIdMapper')
}
if (true || !keycloakDeployment) {
def configStream = ec.getResource().getLocationStream(keycloakJsonUrl)
keycloakDeployment = KeycloakDeploymentBuilder.build(configStream)
cache.put(keycloakJsonUrl, keycloakDeployment)
}
def facade = new OIDCServletHttpFacade(ec.web.request, ec.web.response)
def tokenStore = new OIDCFilterSessionStore(ec.web.request, facade, 100000, keycloakDeployment, idMapper)
def authenticator = new FilterRequestAuthenticator(keycloakDeployment, tokenStore, facade, ec.web.request, 8443)
ec.logger.info("authenticator: ${authenticator}")
def outcome = authenticator.authenticate()
ec.logger.info("rest:outcome: ${outcome}")
ec.logger.info("request: ${ec.web.requestDetails}")
if (outcome == AuthOutcome.AUTHENTICATED) {
if (facade.isEnded()) {
ec.logger.info('facade has ended')
} else {
def actions = new AuthenticatedActionsHandler(keycloakDeployment, facade)
ec.logger.info("actions: ${actions}")
if (actions.handledRequest()) {
ec.logger.info("actions.handledRequest")
sri.stopRender()
return
}
}
} else {
AuthChallenge challenge = authenticator.getChallenge()
ec.logger.info("challenge: ${challenge}")
if (challenge != null) {
if (challenge.challenge(facade)) {
//sri.stopRender()
//return
}
}
}
def ksc = ec.web.request.getAttribute(KeycloakSecurityContext.class.getName())
if (ksc) {
def idToken = ksc.getIdToken()
ec.logger.info("idToken: ${idToken}")
if (idToken) {
def subject = idToken.getSubject()
ec.logger.info("subject: ${subject}")
}
def accessToken = ksc.getToken()
ec.logger.info("accessToken: ${accessToken}")
if (accessToken) {
def subject = accessToken.getSubject()
EntityValue userAccount = ec.entity.find('UserAccount').condition('externalUserId', subject).disableAuthz().one()
ec.logger.info("userAccount: ${userAccount}")
if (userAccount) {
//ec.user.pushUser(userAccount.username)
ec.user.internalLoginUser(userAccount.username)
}
}
}
package org.moqui.keycloak;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class MoquiKeycloakSecurityFilter implements Filter {
@Override
public void init(FilterConfig config) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
} else {
chain.doFilter(request, response);
}
}
public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
OIDCServletHttpFacade facade = new OIDCServletHttpFacade(request, response)
KeycloakDeployment keycloakDeployment = getKeycloakDeployment()
OIDCFilterSessionStore tokenStore = new OIDCFilterSessionStore(request, facade, 100000, keycloakDeployment, idMapper)
// TODO: Look at the PolicyEnforcer stuff in keycloak, perhaps hook into that for moqui
// if the thing being called doesn't require AUTH, then the enforcer should return AUTHENTICATED?
FilterRequestAuthenticator authenticator = new FilterRequestAuthenticator(keycloakDeployment, tokenStore, facade, request, 8443)
AuthOutcome outcome = authenticator.authenticate()
if (outcome == AuthOutcome.AUTHENTICATED) {
if (facade.isEnded()) {
return;
}
AuthenticatedActionsHandler actions = new AuthenticatedActionsHandler(keycloakDeployment, facade);
if (actions.handledRequest()) {
return;
} else {
HttpServletRequestWrapper wrapper = tokenStore.buildWrapper();
postKeycloakFilter(wrapper, response, chain);
return;
}
}
AuthChallenge challenge = authenticator.getChallenge();
if (challenge != null) {
if (request.getRequestURI().equals('/Login')) {
challenge.challenge(facade);
return;
}
if (request.getMethod().equals('GET')) {
challenge.challenge(facade);
return;
}
// TODO
//challenge.challenge(facade);
//return;
}
// TODO
// sendError(403)
postKeycloakFilter(request, response, chain);
}
protected void postKeycloakFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// Can also look at the session if needed
KeycloakSecurityContext ksc = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
logger.debug("doFilter(" + ksc + ")");
//showKeycloakSecurityContext(ksc);
//importKeycloakSecurityContext(ksc);
String username = null;
if (ksc != null) {
ExecutionContext ec = Moqui.getExecutionContext();
//ec.user.pushUser('keycloak-api');
try {
// FIXME: This is bad, I don't know how to force a login without a password.
ec.getUser().loginUser('keycloak-api', 'moqui');
Map<String, Object> result = ec.service.sync().name("bf.auth.KeycloakServices.import#KeycloakUser").parameters([ksc: ksc]).call();
logger.debug('result=' + result)
username = result?.userAccount?.username
request.setAttribute('moqui.request.authenticated', 'true')
} finally {
//ec.user.popUser();
}
}
shiroRunAs(username, request.getSession(), { chain.doFilter(request, response) });
}
}
......@@ -57,11 +57,12 @@ class KeycloakToolFactory implements ToolFactory<Keycloak> {
@Override
void preFacadeInit(ExecutionContextFactory ecf) {
logger.info("Configuring keycloakBuilder")
this.keycloakBuilder = KeycloakBuilder.builder()
.serverUrl((String) System.getProperty("moqui_keycloak_server_url"))
.realm((String) System.getProperty("moqui_keycloak_realm"))
.serverUrl(getServerUrl())
.realm(getRealmName())
.grantType((String) OAuth2Constants.CLIENT_CREDENTIALS)
.clientId((String) System.getProperty("moqui_keycloak_client_id"))
.clientId(getClientId())
.clientSecret((String) System.getProperty("moqui_keycloak_client_secret"))
}
......@@ -70,10 +71,18 @@ class KeycloakToolFactory implements ToolFactory<Keycloak> {
return keycloakBuilder.build()
}
String getClientId() {
return (String) System.getProperty("moqui_keycloak_client_id")
}
String getRealmName() {
return (String) System.getProperty("moqui_keycloak_realm")
}
String getServerUrl() {
return (String) System.getProperty("moqui_keycloak_server_url")
}
@Override
void destroy() {
}
......