View Javadoc
1   /*
2    * Copyright 2012-2021 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
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         // validate that state in response equals to state in request
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 }