diff --git a/src/main/java/org/codelibs/fess/app/web/base/login/AzureAdCredential.java b/src/main/java/org/codelibs/fess/app/web/base/login/AzureAdCredential.java index 7fccb409d..0ccce0490 100644 --- a/src/main/java/org/codelibs/fess/app/web/base/login/AzureAdCredential.java +++ b/src/main/java/org/codelibs/fess/app/web/base/login/AzureAdCredential.java @@ -15,11 +15,13 @@ */ package org.codelibs.fess.app.web.base.login; +import static org.codelibs.core.stream.StreamUtil.split; import static org.codelibs.core.stream.StreamUtil.stream; import java.util.HashSet; import java.util.Set; +import org.codelibs.core.lang.StringUtil; import org.codelibs.fess.entity.FessUser; import org.codelibs.fess.helper.SystemHelper; import org.codelibs.fess.util.ComponentUtil; @@ -46,7 +48,25 @@ public class AzureAdCredential implements LoginCredential, FessCredential { } public User getUser() { - return new User(authResult.getUserInfo().getDisplayableId(), new String[0], new String[0]); + return new User(authResult.getUserInfo().getDisplayableId(), getDefaultGroupsAsArray(), getDefaultRolesAsArray()); + } + + protected static String[] getDefaultGroupsAsArray() { + final String value = ComponentUtil.getFessConfig().getSystemProperty("azuread.default.groups"); + if (StringUtil.isBlank(value)) { + return StringUtil.EMPTY_STRINGS; + } else { + return split(value, ",").get(stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim()).toArray(n -> new String[n])); + } + } + + protected static String[] getDefaultRolesAsArray() { + final String value = ComponentUtil.getFessConfig().getSystemProperty("azuread.default.roles"); + if (StringUtil.isBlank(value)) { + return StringUtil.EMPTY_STRINGS; + } else { + return split(value, ",").get(stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim()).toArray(n -> new String[n])); + } } public static class User implements FessUser { diff --git a/src/main/java/org/codelibs/fess/app/web/base/login/OpenIdConnectCredential.java b/src/main/java/org/codelibs/fess/app/web/base/login/OpenIdConnectCredential.java index 9d391d1b2..52c68f194 100644 --- a/src/main/java/org/codelibs/fess/app/web/base/login/OpenIdConnectCredential.java +++ b/src/main/java/org/codelibs/fess/app/web/base/login/OpenIdConnectCredential.java @@ -15,15 +15,16 @@ */ package org.codelibs.fess.app.web.base.login; +import static org.codelibs.core.stream.StreamUtil.split; import static org.codelibs.core.stream.StreamUtil.stream; import java.util.HashSet; import java.util.Map; import java.util.Set; +import org.codelibs.core.lang.StringUtil; import org.codelibs.fess.entity.FessUser; import org.codelibs.fess.helper.SystemHelper; -import org.codelibs.fess.mylasta.direction.FessConfig; import org.codelibs.fess.util.ComponentUtil; import org.lastaflute.web.login.credential.LoginCredential; @@ -46,8 +47,25 @@ public class OpenIdConnectCredential implements LoginCredential, FessCredential } public User getUser() { - final FessConfig fessConfig = ComponentUtil.getFessConfig(); - return new User(getUserId(), fessConfig.getOicDefaultGroupsAsArray(), fessConfig.getOicDefaultRolesAsArray()); + return new User(getUserId(), getDefaultGroupsAsArray(), getDefaultRolesAsArray()); + } + + protected static String[] getDefaultGroupsAsArray() { + final String value = ComponentUtil.getFessConfig().getSystemProperty("oic.default.groups"); + if (StringUtil.isBlank(value)) { + return StringUtil.EMPTY_STRINGS; + } else { + return split(value, ",").get(stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim()).toArray(n -> new String[n])); + } + } + + protected static String[] getDefaultRolesAsArray() { + final String value = ComponentUtil.getFessConfig().getSystemProperty("oic.default.roles"); + if (StringUtil.isBlank(value)) { + return StringUtil.EMPTY_STRINGS; + } else { + return split(value, ",").get(stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim()).toArray(n -> new String[n])); + } } public static class User implements FessUser { diff --git a/src/main/java/org/codelibs/fess/mylasta/direction/FessProp.java b/src/main/java/org/codelibs/fess/mylasta/direction/FessProp.java index a4ed7e795..a4a296de4 100644 --- a/src/main/java/org/codelibs/fess/mylasta/direction/FessProp.java +++ b/src/main/java/org/codelibs/fess/mylasta/direction/FessProp.java @@ -120,10 +120,6 @@ public interface FessProp { String INDEX_ADMIN_DOUBLE_FIELD_SET = "indexAdminDoubleFieldSet"; - String OIC_DEFAULT_ROLES = "oicDefaultRoles"; - - String OIC_DEFAULT_GROUPS = "oicDefaultGroups"; - String AUTHENTICATION_ADMIN_ROLES = "authenticationAdminRoles"; String SEARCH_GUEST_PERMISSION_LIST = "searchGuestPermissionList"; @@ -1203,38 +1199,6 @@ public interface FessProp { return list; } - default String[] getOicDefaultGroupsAsArray() { - String[] array = (String[]) propMap.get(OIC_DEFAULT_GROUPS); - if (array == null) { - final String oicDefaultGroups = getSystemProperty("oic.default.groups"); - if (StringUtil.isBlank(oicDefaultGroups)) { - array = StringUtil.EMPTY_STRINGS; - } else { - array = - split(oicDefaultGroups, ",").get( - stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim()).toArray(n -> new String[n])); - } - propMap.put(OIC_DEFAULT_GROUPS, array); - } - return array; - } - - default String[] getOicDefaultRolesAsArray() { - String[] array = (String[]) propMap.get(OIC_DEFAULT_ROLES); - if (array == null) { - final String oicDefaultRoles = getSystemProperty("oic.default.roles"); - if (StringUtil.isBlank(oicDefaultRoles)) { - array = StringUtil.EMPTY_STRINGS; - } else { - array = - split(oicDefaultRoles, ",").get( - stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim()).toArray(n -> new String[n])); - } - propMap.put(OIC_DEFAULT_ROLES, array); - } - return array; - } - String getIndexAdminArrayFields(); default Set getIndexAdminArrayFieldSet() { diff --git a/src/main/java/org/codelibs/fess/sso/aad/AzureAdAuthenticator.java b/src/main/java/org/codelibs/fess/sso/aad/AzureAdAuthenticator.java index c5dbe38b0..b20b14f64 100644 --- a/src/main/java/org/codelibs/fess/sso/aad/AzureAdAuthenticator.java +++ b/src/main/java/org/codelibs/fess/sso/aad/AzureAdAuthenticator.java @@ -34,7 +34,6 @@ import org.codelibs.core.net.UuidUtil; import org.codelibs.fess.app.web.base.login.ActionResponseCredential; import org.codelibs.fess.app.web.base.login.AzureAdCredential; import org.codelibs.fess.app.web.base.login.FessLoginAssist.LoginCredentialResolver; -import org.codelibs.fess.app.web.base.login.OpenIdConnectCredential; import org.codelibs.fess.crawler.Constants; import org.codelibs.fess.exception.SsoLoginException; import org.codelibs.fess.sso.SsoAuthenticator; @@ -73,7 +72,7 @@ public class AzureAdAuthenticator implements SsoAuthenticator { protected static final String STATES = "aadStates"; - protected static final String STATE = "aadState"; + protected static final String STATE = "state"; protected static final String ERROR = "error"; @@ -115,10 +114,15 @@ public class AzureAdAuthenticator implements SsoAuthenticator { final String state = UuidUtil.create(); final String nonce = UuidUtil.create(); storeStateInSession(request.getSession(), state, nonce); - return getAuthority() + getTenant() - + "/oauth2/authorize?response_type=code&scope=directory.read.all&response_mode=form_post&redirect_uri=" - + URLEncoder.encode(request.getRequestURL().toString(), Constants.UTF_8_CHARSET) + "&client_id=" + getClientId() - + "&resource=https%3a%2f%2fgraph.microsoft.com" + "&state=" + state + "&nonce=" + nonce; + final String authUrl = + getAuthority() + getTenant() + + "/oauth2/authorize?response_type=code&scope=directory.read.all&response_mode=form_post&redirect_uri=" + + URLEncoder.encode(request.getRequestURL().toString(), Constants.UTF_8_CHARSET) + "&client_id=" + getClientId() + + "&resource=https%3a%2f%2fgraph.microsoft.com" + "&state=" + state + "&nonce=" + nonce; + if (logger.isDebugEnabled()) { + logger.debug("redirect to: {}", authUrl); + } + return authUrl; } @@ -129,7 +133,11 @@ public class AzureAdAuthenticator implements SsoAuthenticator { stateMap = new HashMap<>(); session.setAttribute(STATES, stateMap); } - stateMap.put(state, new StateData(nonce, System.currentTimeMillis())); + final StateData stateData = new StateData(nonce, System.currentTimeMillis()); + if (logger.isDebugEnabled()) { + logger.debug("store {} in session", stateData); + } + stateMap.put(state, stateData); } protected LoginCredential processAuthenticationData(final HttpServletRequest request) { @@ -145,19 +153,21 @@ public class AzureAdAuthenticator implements SsoAuthenticator { params.put(e.getKey(), e.getValue()[0]); } } + if (logger.isDebugEnabled()) { + logger.debug("process authentication: url: {}, params: {}", urlBuf, params); + } // validate that state in response equals to state in request final StateData stateData = validateState(request.getSession(), params.get(STATE)); + if (logger.isDebugEnabled()) { + logger.debug("load {}", stateData); + } final AuthenticationResponse authResponse = parseAuthenticationResponse(urlBuf.toString(), params); if (authResponse instanceof AuthenticationSuccessResponse) { final AuthenticationSuccessResponse oidcResponse = (AuthenticationSuccessResponse) authResponse; - // validate that OIDC Auth Response matches Code Flow (contains only requested artifacts) validateAuthRespMatchesCodeFlow(oidcResponse); - final AuthenticationResult authData = getAccessToken(oidcResponse.getAuthorizationCode(), request.getRequestURL().toString()); - // validate nonce to prevent reply attacks (code maybe substituted to one with broader access) - validateNonce(stateData, authData); return new AzureAdCredential(authData); @@ -169,6 +179,9 @@ public class AzureAdAuthenticator implements SsoAuthenticator { } protected AuthenticationResponse parseAuthenticationResponse(final String url, final Map params) { + if (logger.isDebugEnabled()) { + logger.debug("Parse: {} : {}", url, params); + } try { return AuthenticationResponseParser.parse(new URI(url), params); } catch (final Exception e) { @@ -178,6 +191,9 @@ public class AzureAdAuthenticator implements SsoAuthenticator { protected void validateNonce(final StateData stateData, final AuthenticationResult authData) { final String idToken = authData.getIdToken(); + if (logger.isDebugEnabled()) { + logger.debug("idToken: {}", idToken); + } try { final JWTClaimsSet claimsSet = JWTParser.parse(idToken).getJWTClaimsSet(); if (claimsSet == null) { @@ -185,6 +201,9 @@ public class AzureAdAuthenticator implements SsoAuthenticator { } final String nonce = (String) claimsSet.getClaim("nonce"); + if (logger.isDebugEnabled()) { + logger.debug("nonce: {}", nonce); + } if (StringUtils.isEmpty(nonce) || !nonce.equals(stateData.getNonce())) { throw new SsoLoginException("could not validate nonce"); } @@ -196,12 +215,16 @@ public class AzureAdAuthenticator implements SsoAuthenticator { } protected AuthenticationResult getAccessToken(final AuthorizationCode authorizationCode, final String currentUri) { + final String authority = getAuthority() + getTenant() + "/"; final String authCode = authorizationCode.getValue(); + if (logger.isDebugEnabled()) { + logger.debug("authCode: {}, authority: {}, uri: {}", authCode, authority, currentUri); + } final ClientCredential credential = new ClientCredential(getClientId(), getClientSecret()); ExecutorService service = null; try { service = Executors.newFixedThreadPool(1);// TODO replace with something... - final AuthenticationContext context = new AuthenticationContext(getAuthority() + getTenant() + "/", true, service); + final AuthenticationContext context = new AuthenticationContext(authority, true, service); final Future future = context.acquireTokenByAuthorizationCode(authCode, new URI(currentUri), credential, null); final AuthenticationResult result = future.get(); @@ -240,9 +263,17 @@ public class AzureAdAuthenticator implements SsoAuthenticator { if (states != null) { final long now = System.currentTimeMillis(); states.entrySet().stream().filter(e -> (now - e.getValue().getExpiration()) / 1000L > getStateTtl()).map(Map.Entry::getKey) - .collect(Collectors.toList()).forEach(s -> states.remove(s)); + .collect(Collectors.toList()).forEach(s -> { + if (logger.isDebugEnabled()) { + logger.debug("remove old state: {}", s); + } + states.remove(s); + }); final StateData stateData = states.get(state); if (stateData != null) { + if (logger.isDebugEnabled()) { + logger.debug("restore {} from session", stateData); + } states.remove(state); return stateData; } @@ -274,6 +305,11 @@ public class AzureAdAuthenticator implements SsoAuthenticator { public long getExpiration() { return expiration; } + + @Override + public String toString() { + return "StateData [nonce=" + nonce + ", expiration=" + expiration + "]"; + } } protected String getClientId() { @@ -289,7 +325,7 @@ public class AzureAdAuthenticator implements SsoAuthenticator { } protected String getAuthority() { - return ComponentUtil.getFessConfig().getSystemProperty(AZUREAD_AUTHORITY, StringUtil.EMPTY); + return ComponentUtil.getFessConfig().getSystemProperty(AZUREAD_AUTHORITY, "https://login.microsoftonline.com/"); } protected long getStateTtl() { @@ -298,7 +334,7 @@ public class AzureAdAuthenticator implements SsoAuthenticator { @Override public void resolveCredential(final LoginCredentialResolver resolver) { - resolver.resolve(OpenIdConnectCredential.class, credential -> { + resolver.resolve(AzureAdCredential.class, credential -> { return OptionalEntity.of(credential.getUser()); }); } diff --git a/src/main/java/org/codelibs/fess/sso/spnego/SpnegoAuthenticator.java b/src/main/java/org/codelibs/fess/sso/spnego/SpnegoAuthenticator.java index 2604b0855..fa45f1a63 100644 --- a/src/main/java/org/codelibs/fess/sso/spnego/SpnegoAuthenticator.java +++ b/src/main/java/org/codelibs/fess/sso/spnego/SpnegoAuthenticator.java @@ -29,6 +29,7 @@ import org.codelibs.fess.app.web.base.login.ActionResponseCredential; import org.codelibs.fess.app.web.base.login.FessLoginAssist.LoginCredentialResolver; import org.codelibs.fess.app.web.base.login.SpnegoCredential; import org.codelibs.fess.exception.SsoLoginException; +import org.codelibs.fess.mylasta.direction.FessConfig; import org.codelibs.fess.sso.SsoAuthenticator; import org.codelibs.fess.util.ComponentUtil; import org.codelibs.spnego.SpnegoFilterConfig; @@ -45,8 +46,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SpnegoAuthenticator implements SsoAuthenticator { + private static final Logger logger = LoggerFactory.getLogger(SpnegoAuthenticator.class); + protected static final String SPNEGO_INITIALIZED = "spnego.initialized"; protected static final String SPNEGO_EXCLUDE_DIRS = "spnego.exclude.dirs"; protected static final String SPNEGO_ALLOW_DELEGATION = "spnego.allow.delegation"; protected static final String SPNEGO_ALLOW_LOCALHOST = "spnego.allow.localhost"; @@ -65,6 +68,14 @@ public class SpnegoAuthenticator implements SsoAuthenticator { @PostConstruct public void init() { + ComponentUtil.getSsoManager().register(this); + } + + protected synchronized org.codelibs.spnego.SpnegoAuthenticator getAuthenticator() { + final FessConfig fessConfig = ComponentUtil.getFessConfig(); + if (authenticator != null && fessConfig.getSystemPropertyAsBoolean(SPNEGO_INITIALIZED, false)) { + return authenticator; + } try { // set some System properties final SpnegoFilterConfig config = SpnegoFilterConfig.getInstance(new SpengoConfig()); @@ -72,13 +83,11 @@ public class SpnegoAuthenticator implements SsoAuthenticator { // pre-authenticate authenticator = new org.codelibs.spnego.SpnegoAuthenticator(config); - ComponentUtil.getSsoManager().register(this); + fessConfig.setSystemPropertyAsBoolean(SPNEGO_INITIALIZED, true); + fessConfig.storeSystemProperties(); + return authenticator; } catch (final Exception e) { - if (logger.isDebugEnabled()) { - logger.warn("Failed to initialize SPNEGO.", e); - } else { - logger.warn("Failed to initialize SPNEGO."); - } + throw new SsoLoginException("Failed to initialize SPNEGO.", e); } } @@ -96,7 +105,7 @@ public class SpnegoAuthenticator implements SsoAuthenticator { // client/caller principal final SpnegoPrincipal principal; try { - principal = authenticator.authenticate(request, spnegoResponse); + principal = getAuthenticator().authenticate(request, spnegoResponse); } catch (final Exception e) { final String msg = "HTTP Authorization Header=" + request.getHeader(Constants.AUTHZ_HEADER); if (logger.isDebugEnabled()) {