1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.sso.aad;
17
18 import static org.codelibs.core.stream.StreamUtil.split;
19
20 import java.io.IOException;
21 import java.net.URI;
22 import java.net.URLEncoder;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collections;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Locale;
29 import java.util.Map;
30 import java.util.concurrent.ExecutionException;
31 import java.util.concurrent.ExecutorService;
32 import java.util.concurrent.Executors;
33 import java.util.concurrent.Future;
34 import java.util.concurrent.TimeUnit;
35 import java.util.stream.Collectors;
36
37 import javax.annotation.PostConstruct;
38 import javax.servlet.http.HttpServletRequest;
39 import javax.servlet.http.HttpSession;
40
41 import org.apache.commons.lang3.StringUtils;
42 import org.apache.logging.log4j.LogManager;
43 import org.apache.logging.log4j.Logger;
44 import org.codelibs.core.lang.StringUtil;
45 import org.codelibs.core.misc.Pair;
46 import org.codelibs.core.net.UuidUtil;
47 import org.codelibs.core.stream.StreamUtil;
48 import org.codelibs.curl.Curl;
49 import org.codelibs.curl.CurlResponse;
50 import org.codelibs.fesen.runner.net.FesenCurl;
51 import org.codelibs.fess.app.web.base.login.ActionResponseCredential;
52 import org.codelibs.fess.app.web.base.login.AzureAdCredential;
53 import org.codelibs.fess.app.web.base.login.AzureAdCredential.AzureAdUser;
54 import org.codelibs.fess.app.web.base.login.FessLoginAssist.LoginCredentialResolver;
55 import org.codelibs.fess.crawler.Constants;
56 import org.codelibs.fess.exception.SsoLoginException;
57 import org.codelibs.fess.mylasta.action.FessUserBean;
58 import org.codelibs.fess.mylasta.direction.FessConfig;
59 import org.codelibs.fess.sso.SsoAuthenticator;
60 import org.codelibs.fess.sso.SsoResponseType;
61 import org.codelibs.fess.util.ComponentUtil;
62 import org.codelibs.fess.util.DocumentUtil;
63 import org.dbflute.optional.OptionalEntity;
64 import org.lastaflute.web.login.credential.LoginCredential;
65 import org.lastaflute.web.response.ActionResponse;
66 import org.lastaflute.web.response.HtmlResponse;
67 import org.lastaflute.web.util.LaRequestUtil;
68
69 import com.google.common.cache.Cache;
70 import com.google.common.cache.CacheBuilder;
71 import com.microsoft.aad.adal4j.AuthenticationContext;
72 import com.microsoft.aad.adal4j.AuthenticationResult;
73 import com.microsoft.aad.adal4j.ClientCredential;
74 import com.nimbusds.jwt.JWTClaimsSet;
75 import com.nimbusds.jwt.JWTParser;
76 import com.nimbusds.oauth2.sdk.AuthorizationCode;
77 import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
78 import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
79 import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser;
80 import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
81
82 public class AzureAdAuthenticator implements SsoAuthenticator {
83
84 private static final Logger logger = LogManager.getLogger(AzureAdAuthenticator.class);
85
86 protected static final String AZUREAD_STATE_TTL = "aad.state.ttl";
87
88 protected static final String AZUREAD_AUTHORITY = "aad.authority";
89
90 protected static final String AZUREAD_TENANT = "aad.tenant";
91
92 protected static final String AZUREAD_CLIENT_SECRET = "aad.client.secret";
93
94 protected static final String AZUREAD_CLIENT_ID = "aad.client.id";
95
96 protected static final String AZUREAD_REPLY_URL = "aad.reply.url";
97
98 protected static final String STATES = "aadStates";
99
100 protected static final String STATE = "state";
101
102 protected static final String ERROR = "error";
103
104 protected static final String ERROR_DESCRIPTION = "error_description";
105
106 protected static final String ERROR_URI = "error_uri";
107
108 protected static final String ID_TOKEN = "id_token";
109
110 protected static final String CODE = "code";
111
112 protected long acquisitionTimeout = 30 * 1000L;
113
114 protected Cache<String, Pair<String[], String[]>> groupCache;
115
116 protected long groupCacheExpiry = 10 * 60L;
117
118 @PostConstruct
119 public void init() {
120 if (logger.isDebugEnabled()) {
121 logger.debug("Initialize {}", this.getClass().getSimpleName());
122 }
123 ComponentUtil.getSsoManager().register(this);
124 groupCache = CacheBuilder.newBuilder().expireAfterWrite(groupCacheExpiry, TimeUnit.SECONDS).build();
125 }
126
127 @Override
128 public LoginCredential getLoginCredential() {
129 return LaRequestUtil.getOptionalRequest().map(request -> {
130 if (logger.isDebugEnabled()) {
131 logger.debug("Logging in with Azure AD Authenticator");
132 }
133 final HttpSession session = request.getSession(false);
134 if (session != null && containsAuthenticationData(request)) {
135 try {
136 return processAuthenticationData(request);
137 } catch (final Exception e) {
138 if (logger.isDebugEnabled()) {
139 logger.debug("Failed to process a login request on AzureAD.", e);
140 }
141 }
142 return null;
143 }
144
145 return new ActionResponseCredential(() -> HtmlResponse.fromRedirectPathAsIs(getAuthUrl(request)));
146 }).orElse(null);
147 }
148
149 protected String getAuthUrl(final HttpServletRequest request) {
150 final String state = UuidUtil.create();
151 final String nonce = UuidUtil.create();
152 storeStateInSession(request.getSession(), state, nonce);
153 final String authUrl = getAuthority() + getTenant()
154 + "/oauth2/authorize?response_type=code&scope=directory.read.all&response_mode=form_post&redirect_uri="
155 + URLEncoder.encode(getReplyUrl(request), Constants.UTF_8_CHARSET) + "&client_id=" + getClientId()
156 + "&resource=https%3a%2f%2fgraph.microsoft.com" + "&state=" + state + "&nonce=" + nonce;
157 if (logger.isDebugEnabled()) {
158 logger.debug("redirect to: {}", authUrl);
159 }
160 return authUrl;
161
162 }
163
164 protected void storeStateInSession(final HttpSession session, final String state, final String nonce) {
165 @SuppressWarnings("unchecked")
166 Map<String, StateData> stateMap = (Map<String, StateData>) session.getAttribute(STATES);
167 if (stateMap == null) {
168 stateMap = new HashMap<>();
169 session.setAttribute(STATES, stateMap);
170 }
171 final StateData stateData = new StateData(nonce, System.currentTimeMillis());
172 if (logger.isDebugEnabled()) {
173 logger.debug("store {} in session", stateData);
174 }
175 stateMap.put(state, stateData);
176 }
177
178 protected LoginCredential processAuthenticationData(final HttpServletRequest request) {
179 final StringBuffer urlBuf = request.getRequestURL();
180 final String queryStr = request.getQueryString();
181 if (queryStr != null) {
182 urlBuf.append('?').append(queryStr);
183 }
184
185 final Map<String, List<String>> params = new HashMap<>();
186 for (final Map.Entry<String, String[]> e : request.getParameterMap().entrySet()) {
187 if (e.getValue().length > 0) {
188 params.put(e.getKey(), Arrays.asList(e.getValue()));
189 }
190 }
191 if (logger.isDebugEnabled()) {
192 logger.debug("process authentication: url: {}, params: {}", urlBuf, params);
193 }
194
195
196 final StateData stateData = validateState(request.getSession(), params.containsKey(STATE) ? params.get(STATE).get(0) : null);
197 if (logger.isDebugEnabled()) {
198 logger.debug("load {}", stateData);
199 }
200
201 final AuthenticationResponse authResponse = parseAuthenticationResponse(urlBuf.toString(), params);
202 if (authResponse instanceof AuthenticationSuccessResponse) {
203 final AuthenticationSuccessResponse oidcResponse = (AuthenticationSuccessResponse) authResponse;
204 validateAuthRespMatchesCodeFlow(oidcResponse);
205 final AuthenticationResult authData = getAccessToken(oidcResponse.getAuthorizationCode(), getReplyUrl(request));
206 validateNonce(stateData, authData);
207
208 return new AzureAdCredential(authData);
209 }
210 final AuthenticationErrorResponse oidcResponse = (AuthenticationErrorResponse) authResponse;
211 throw new SsoLoginException(String.format("Request for auth code failed: %s - %s", oidcResponse.getErrorObject().getCode(),
212 oidcResponse.getErrorObject().getDescription()));
213 }
214
215 protected AuthenticationResponse parseAuthenticationResponse(final String url, final Map<String, List<String>> params) {
216 if (logger.isDebugEnabled()) {
217 logger.debug("Parse: {} : {}", url, params);
218 }
219 try {
220 return AuthenticationResponseParser.parse(new URI(url), params);
221 } catch (final Exception e) {
222 throw new SsoLoginException("Failed to parse an authentication response.", e);
223 }
224 }
225
226 protected void validateNonce(final StateData stateData, final AuthenticationResult authData) {
227 final String idToken = authData.getIdToken();
228 if (logger.isDebugEnabled()) {
229 logger.debug("idToken: {}", idToken);
230 }
231 try {
232 final JWTClaimsSet claimsSet = JWTParser.parse(idToken).getJWTClaimsSet();
233 if (claimsSet == null) {
234 throw new SsoLoginException("could not validate nonce");
235 }
236
237 final String nonce = (String) claimsSet.getClaim("nonce");
238 if (logger.isDebugEnabled()) {
239 logger.debug("nonce: {}", nonce);
240 }
241 if (StringUtils.isEmpty(nonce) || !nonce.equals(stateData.getNonce())) {
242 throw new SsoLoginException("could not validate nonce");
243 }
244 } catch (final SsoLoginException e) {
245 throw e;
246 } catch (final Exception e) {
247 throw new SsoLoginException("could not validate nonce", e);
248 }
249 }
250
251 public AuthenticationResult getAccessToken(final String refreshToken) {
252 final String authority = getAuthority() + getTenant() + "/";
253 if (logger.isDebugEnabled()) {
254 logger.debug("refreshToken: {}, authority: {}", refreshToken, authority);
255 }
256 ExecutorService service = null;
257 try {
258 service = Executors.newFixedThreadPool(1);
259 final AuthenticationContext context = new AuthenticationContext(authority, true, service);
260 final Future<AuthenticationResult> future =
261 context.acquireTokenByRefreshToken(refreshToken, new ClientCredential(getClientId(), getClientSecret()), null, null);
262 final AuthenticationResult result = future.get(acquisitionTimeout, TimeUnit.MILLISECONDS);
263 if (result == null) {
264 throw new SsoLoginException("authentication result was null");
265 }
266 return result;
267 } catch (final Exception e) {
268 throw new SsoLoginException("Failed to get a token.", e);
269 } finally {
270 if (service != null) {
271 service.shutdown();
272 }
273 }
274 }
275
276 protected AuthenticationResult getAccessToken(final AuthorizationCode authorizationCode, final String currentUri) {
277 final String authority = getAuthority() + getTenant() + "/";
278 final String authCode = authorizationCode.getValue();
279 if (logger.isDebugEnabled()) {
280 logger.debug("authCode: {}, authority: {}, uri: {}", authCode, authority, currentUri);
281 }
282 final ClientCredential credential = new ClientCredential(getClientId(), getClientSecret());
283 ExecutorService service = null;
284 try {
285 service = Executors.newFixedThreadPool(1);
286 final AuthenticationContext context = new AuthenticationContext(authority, true, service);
287 final Future<AuthenticationResult> future =
288 context.acquireTokenByAuthorizationCode(authCode, new URI(currentUri), credential, null);
289 final AuthenticationResult result = future.get(acquisitionTimeout, TimeUnit.MILLISECONDS);
290 if (result == null) {
291 throw new SsoLoginException("authentication result was null");
292 }
293 return result;
294 } catch (final Exception e) {
295 throw new SsoLoginException("Failed to get a token.", e);
296 } finally {
297 if (service != null) {
298 service.shutdown();
299 }
300 }
301 }
302
303 protected void validateAuthRespMatchesCodeFlow(final AuthenticationSuccessResponse oidcResponse) {
304 if (oidcResponse.getIDToken() != null || oidcResponse.getAccessToken() != null || oidcResponse.getAuthorizationCode() == null) {
305 throw new SsoLoginException("unexpected set of artifacts received");
306 }
307 }
308
309 protected StateData validateState(final HttpSession session, final String state) {
310 if (StringUtils.isNotEmpty(state)) {
311 final StateData stateDataInSession = removeStateFromSession(session, state);
312 if (stateDataInSession != null) {
313 return stateDataInSession;
314 }
315 }
316 throw new SsoLoginException("could not validate state");
317 }
318
319 protected StateData removeStateFromSession(final HttpSession session, final String state) {
320 @SuppressWarnings("unchecked")
321 final Map<String, StateData> states = (Map<String, StateData>) session.getAttribute(STATES);
322 if (states != null) {
323 final long now = System.currentTimeMillis();
324 states.entrySet().stream().filter(e -> (now - e.getValue().getExpiration()) / 1000L > getStateTtl()).map(Map.Entry::getKey)
325 .collect(Collectors.toList()).forEach(s -> {
326 if (logger.isDebugEnabled()) {
327 logger.debug("remove old state: {}", s);
328 }
329 states.remove(s);
330 });
331 final StateData stateData = states.get(state);
332 if (stateData != null) {
333 if (logger.isDebugEnabled()) {
334 logger.debug("restore {} from session", stateData);
335 }
336 states.remove(state);
337 return stateData;
338 }
339 }
340 return null;
341 }
342
343 protected boolean containsAuthenticationData(final HttpServletRequest request) {
344 if (logger.isDebugEnabled()) {
345 logger.debug("HTTP Method: {}", request.getMethod());
346 }
347 if (!"POST".equalsIgnoreCase(request.getMethod())) {
348 return false;
349 }
350 final Map<String, String[]> params = request.getParameterMap();
351 if (logger.isDebugEnabled()) {
352 logger.debug("params: {}", params);
353 }
354 return params.containsKey(ERROR) || params.containsKey(ID_TOKEN) || params.containsKey(CODE);
355 }
356
357 public void updateMemberOf(final AzureAdUser user) {
358 final List<String> groupList = new ArrayList<>();
359 final List<String> roleList = new ArrayList<>();
360 groupList.addAll(getDefaultGroupList());
361 roleList.addAll(getDefaultRoleList());
362 processMemberOf(user, groupList, roleList, "https://graph.microsoft.com/v1.0/me/memberOf");
363 user.setGroups(groupList.stream().distinct().toArray(n -> new String[n]));
364 user.setRoles(roleList.stream().distinct().toArray(n -> new String[n]));
365 }
366
367 protected void processMemberOf(final AzureAdUser user, final List<String> groupList, final List<String> roleList, final String url) {
368 if (logger.isDebugEnabled()) {
369 logger.debug("url: {}", url);
370 }
371 try (CurlResponse response = Curl.get(url).header("Authorization", "Bearer " + user.getAuthenticationResult().getAccessToken())
372 .header("Accept", "application/json").execute()) {
373 final Map<String, Object> contentMap = response.getContent(FesenCurl.jsonParser());
374 if (logger.isDebugEnabled()) {
375 logger.debug("response: {}", contentMap);
376 }
377 if (contentMap.containsKey("value")) {
378 @SuppressWarnings("unchecked")
379 final List<Map<String, Object>> memberOfList = (List<Map<String, Object>>) contentMap.get("value");
380 final FessConfig fessConfig = ComponentUtil.getFessConfig();
381 for (final Map<String, Object> memberOf : memberOfList) {
382 if (logger.isDebugEnabled()) {
383 logger.debug("member: {}", memberOf);
384 }
385 String memberType = (String) memberOf.get("@odata.type");
386 if (memberType == null) {
387 logger.warn("@odata.type is null: {}", memberOf);
388 continue;
389 }
390 memberType = memberType.toLowerCase(Locale.ENGLISH);
391 final String id = (String) memberOf.get("id");
392 if (StringUtil.isNotBlank(id)) {
393 if (memberType.contains("group")) {
394 groupList.add(id);
395 } else if (memberType.contains("role")) {
396 roleList.add(id);
397 } else {
398 if (logger.isDebugEnabled()) {
399 logger.debug("unknown @odata.type: {}", memberOf);
400 }
401 groupList.add(id);
402 }
403 processParentGroup(user, groupList, roleList, id);
404 } else {
405 logger.warn("id is empty: {}", memberOf);
406 }
407 final String[] names = fessConfig.getAzureAdPermissionFields();
408 for (final String name : names) {
409 final String value = (String) memberOf.get(name);
410 if (StringUtil.isNotBlank(value)) {
411 if (memberType.contains("group")) {
412 groupList.add(value);
413 } else if (memberType.contains("role")) {
414 roleList.add(value);
415 } else {
416 if (logger.isDebugEnabled()) {
417 logger.debug("unknown @odata.type: {}", memberOf);
418 }
419 groupList.add(value);
420 }
421 } else if (logger.isDebugEnabled()) {
422 logger.debug("{} is empty: {}", name, memberOf);
423 }
424 }
425 }
426 final String nextLink = (String) contentMap.get("@odata.nextLink");
427 if (StringUtil.isNotBlank(nextLink)) {
428 processMemberOf(user, groupList, roleList, nextLink);
429 }
430 } else if (contentMap.containsKey("error")) {
431 logger.warn("Failed to access groups/roles: {}", contentMap);
432 }
433 } catch (final IOException e) {
434 logger.warn("Failed to access groups/roles in AzureAD.", e);
435 }
436 }
437
438 protected void processParentGroup(final AzureAdUser user, final List<String> groupList, final List<String> roleList, final String id) {
439 final Pair<String[], String[]> groupsAndRoles = getParentGroup(user, id);
440 StreamUtil.stream(groupsAndRoles.getFirst()).of(stream -> stream.forEach(groupList::add));
441 StreamUtil.stream(groupsAndRoles.getSecond()).of(stream -> stream.forEach(roleList::add));
442 }
443
444 protected Pair<String[], String[]> getParentGroup(final AzureAdUser user, final String id) {
445 try {
446 return groupCache.get(id, () -> {
447 final List<String> groupList = new ArrayList<>();
448 final List<String> roleList = new ArrayList<>();
449 final String url = "https://graph.microsoft.com/v1.0/groups/" + id + "/getMemberGroups";
450 if (logger.isDebugEnabled()) {
451 logger.debug("url: {}", url);
452 }
453 try (CurlResponse response =
454 Curl.post(url).header("Authorization", "Bearer " + user.getAuthenticationResult().getAccessToken())
455 .header("Accept", "application/json").header("Content-type", "application/json")
456 .body("{\"securityEnabledOnly\":false}").execute()) {
457 final Map<String, Object> contentMap = response.getContent(FesenCurl.jsonParser());
458 if (logger.isDebugEnabled()) {
459 logger.debug("response: {}", contentMap);
460 }
461 if (contentMap.containsKey("value")) {
462 final String[] values = DocumentUtil.getValue(contentMap, "value", String[].class);
463 if (values != null) {
464 for (final String value : values) {
465 processGroup(user, groupList, roleList, value);
466 if (!groupList.contains(value) && !roleList.contains(value)) {
467 final Pair<String[], String[]> groupsAndRoles = getParentGroup(user, value);
468 StreamUtil.stream(groupsAndRoles.getFirst()).of(stream1 -> stream1.forEach(groupList::add));
469 StreamUtil.stream(groupsAndRoles.getSecond()).of(stream2 -> stream2.forEach(roleList::add));
470 }
471 }
472 }
473 } else if (contentMap.containsKey("error")) {
474 logger.warn("Failed to access parent groups: {}", contentMap);
475 }
476 } catch (final IOException e) {
477 logger.warn("Failed to access groups/roles in AzureAD.", e);
478 }
479 return new Pair<>(groupList.stream().distinct().toArray(n1 -> new String[n1]),
480 roleList.stream().distinct().toArray(n2 -> new String[n2]));
481 });
482 } catch (final ExecutionException e) {
483 logger.warn("Failed to process a group cache.", e);
484 return new Pair<>(StringUtil.EMPTY_STRINGS, StringUtil.EMPTY_STRINGS);
485 }
486 }
487
488 protected void processGroup(final AzureAdUser user, final List<String> groupList, final List<String> roleList, final String id) {
489 final String url = "https://graph.microsoft.com/v1.0/groups/" + id;
490 if (logger.isDebugEnabled()) {
491 logger.debug("url: {}", url);
492 }
493 try (CurlResponse response = Curl.get(url).header("Authorization", "Bearer " + user.getAuthenticationResult().getAccessToken())
494 .header("Accept", "application/json").execute()) {
495 final Map<String, Object> contentMap = response.getContent(FesenCurl.jsonParser());
496 if (logger.isDebugEnabled()) {
497 logger.debug("response: {}", contentMap);
498 }
499 groupList.add(id);
500 if (contentMap.containsKey("error")) {
501 logger.warn("Failed to access parent groups: {}", contentMap);
502 } else {
503 final FessConfig fessConfig = ComponentUtil.getFessConfig();
504 final String[] names = fessConfig.getAzureAdPermissionFields();
505 for (final String name : names) {
506 final String value = (String) contentMap.get(name);
507 if (StringUtil.isNotBlank(value)) {
508 groupList.add(value);
509 } else if (logger.isDebugEnabled()) {
510 logger.debug("{} is empty: {}", name, id);
511 }
512 }
513 }
514 } catch (final IOException e) {
515 logger.warn("Failed to access groups/roles in AzureAD.", e);
516 }
517 }
518
519 protected List<String> getDefaultGroupList() {
520 final String value = ComponentUtil.getFessConfig().getSystemProperty("aad.default.groups");
521 if (StringUtil.isBlank(value)) {
522 return Collections.emptyList();
523 }
524 return split(value, ",").get(stream -> stream.filter(StringUtil::isNotBlank).map(String::trim).collect(Collectors.toList()));
525 }
526
527 protected List<String> getDefaultRoleList() {
528 final String value = ComponentUtil.getFessConfig().getSystemProperty("aad.default.roles");
529 if (StringUtil.isBlank(value)) {
530 return Collections.emptyList();
531 }
532 return split(value, ",").get(stream -> stream.filter(StringUtil::isNotBlank).map(String::trim).collect(Collectors.toList()));
533 }
534
535 protected static class StateData {
536 private final String nonce;
537 private final long expiration;
538
539 public StateData(final String nonce, final long expiration) {
540 this.nonce = nonce;
541 this.expiration = expiration;
542 }
543
544 public String getNonce() {
545 return nonce;
546 }
547
548 public long getExpiration() {
549 return expiration;
550 }
551
552 @Override
553 public String toString() {
554 return "StateData [nonce=" + nonce + ", expiration=" + expiration + "]";
555 }
556 }
557
558 protected String getClientId() {
559 return ComponentUtil.getFessConfig().getSystemProperty(AZUREAD_CLIENT_ID, StringUtil.EMPTY);
560 }
561
562 protected String getClientSecret() {
563 return ComponentUtil.getFessConfig().getSystemProperty(AZUREAD_CLIENT_SECRET, StringUtil.EMPTY);
564 }
565
566 protected String getTenant() {
567 return ComponentUtil.getFessConfig().getSystemProperty(AZUREAD_TENANT, StringUtil.EMPTY);
568 }
569
570 protected String getAuthority() {
571 return ComponentUtil.getFessConfig().getSystemProperty(AZUREAD_AUTHORITY, "https://login.microsoftonline.com/");
572 }
573
574 protected long getStateTtl() {
575 return Long.parseLong(ComponentUtil.getFessConfig().getSystemProperty(AZUREAD_STATE_TTL, "3600"));
576 }
577
578 protected String getReplyUrl(final HttpServletRequest request) {
579 final String value = ComponentUtil.getFessConfig().getSystemProperty(AZUREAD_REPLY_URL, StringUtil.EMPTY);
580 if (StringUtil.isNotBlank(value)) {
581 return value;
582 }
583 return request.getRequestURL().toString();
584 }
585
586 @Override
587 public void resolveCredential(final LoginCredentialResolver resolver) {
588 resolver.resolve(AzureAdCredential.class, credential -> OptionalEntity.of(credential.getUser()));
589 }
590
591 public void setAcquisitionTimeout(final long acquisitionTimeout) {
592 this.acquisitionTimeout = acquisitionTimeout;
593 }
594
595 public void setGroupCacheExpiry(final long groupCacheExpiry) {
596 this.groupCacheExpiry = groupCacheExpiry;
597 }
598
599 @Override
600 public ActionResponse getResponse(final SsoResponseType responseType) {
601 return null;
602 }
603
604 @Override
605 public String logout(final FessUserBean user) {
606 return null;
607 }
608 }