View Javadoc
1   /*
2    * Copyright 2012-2025 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.app.web.base.login;
17  
18  import static org.codelibs.core.stream.StreamUtil.stream;
19  
20  import java.util.HashSet;
21  import java.util.Set;
22  
23  import org.apache.logging.log4j.LogManager;
24  import org.apache.logging.log4j.Logger;
25  import org.codelibs.core.lang.StringUtil;
26  import org.codelibs.fess.entity.FessUser;
27  import org.codelibs.fess.helper.SystemHelper;
28  import org.codelibs.fess.sso.entraid.EntraIdAuthenticator;
29  import org.codelibs.fess.util.ComponentUtil;
30  import org.lastaflute.web.login.credential.LoginCredential;
31  
32  import com.microsoft.aad.msal4j.IAccount;
33  import com.microsoft.aad.msal4j.IAuthenticationResult;
34  
35  /**
36   * Microsoft Entra ID credential implementation for Fess authentication.
37   * Provides login credential functionality using Entra ID authentication results.
38   */
39  public class EntraIdCredential implements LoginCredential, FessCredential {
40  
41      private static final Logger logger = LogManager.getLogger(EntraIdCredential.class);
42  
43      private final IAuthenticationResult authResult;
44  
45      /**
46       * Constructs an Entra ID credential with the authentication result.
47       * @param authResult The authentication result from Entra ID.
48       */
49      public EntraIdCredential(final IAuthenticationResult authResult) {
50          this.authResult = authResult;
51      }
52  
53      @Override
54      public String getUserId() {
55          return authResult.account().username();
56      }
57  
58      @Override
59      public String toString() {
60          return "{" + authResult.account().username() + "}";
61      }
62  
63      /**
64       * Gets the Entra ID user associated with this credential.
65       * @return The Entra ID user instance.
66       */
67      public EntraIdUser getUser() {
68          return new EntraIdUser(authResult);
69      }
70  
71      /**
72       * Entra ID user implementation providing user information and permissions.
73       */
74      public static class EntraIdUser implements FessUser {
75          private static final long serialVersionUID = 1L;
76  
77          /** User's group memberships. */
78          protected volatile String[] groups;
79  
80          /** User's role assignments. */
81          protected volatile String[] roles;
82  
83          /** User's computed permissions. */
84          protected volatile String[] permissions;
85  
86          /** Entra ID authentication result. */
87          protected IAuthenticationResult authResult;
88  
89          /**
90           * Constructs an Entra ID user with the authentication result.
91           * @param authResult The authentication result from Entra ID.
92           */
93          public EntraIdUser(final IAuthenticationResult authResult) {
94              this.authResult = authResult;
95              final EntraIdAuthenticator authenticator = ComponentUtil.getComponent(EntraIdAuthenticator.class);
96              authenticator.updateMemberOf(this);
97          }
98  
99          @Override
100         public String getName() {
101             return authResult.account().username();
102         }
103 
104         @Override
105         public String[] getRoleNames() {
106             return roles;
107         }
108 
109         @Override
110         public String[] getGroupNames() {
111             return groups;
112         }
113 
114         @Override
115         public String[] getPermissions() {
116             if (permissions == null) {
117                 final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
118                 final Set<String> permissionSet = new HashSet<>();
119                 final IAccount account = authResult.account();
120                 final String homeAccountId = account.homeAccountId();
121                 final String username = account.username();
122                 if (logger.isDebugEnabled()) {
123                     logger.debug("homeAccountId={}, username={}", homeAccountId, username);
124                 }
125                 permissionSet.add(systemHelper.getSearchRoleByUser(homeAccountId));
126                 permissionSet.add(systemHelper.getSearchRoleByUser(username));
127                 if (ComponentUtil.getFessConfig().isEntraIdUseDomainServices() && username.indexOf('@') >= 0) {
128                     final String[] values = username.split("@");
129                     if (values.length > 1) {
130                         permissionSet.add(systemHelper.getSearchRoleByUser(values[0]));
131                     }
132                 }
133                 stream(groups).of(stream -> stream.forEach(s -> permissionSet.add(systemHelper.getSearchRoleByGroup(s))));
134                 stream(roles).of(stream -> stream.forEach(s -> permissionSet.add(systemHelper.getSearchRoleByRole(s))));
135                 permissions = permissionSet.stream().filter(StringUtil::isNotBlank).distinct().toArray(n -> new String[n]);
136             }
137             return permissions;
138         }
139 
140         @Override
141         public boolean refresh() {
142             // MSAL4J handles token refresh internally through silent authentication
143             // Check if token is still valid by comparing absolute timestamps
144             final long tokenExpiryTime = authResult.expiresOnDate().getTime(); // milliseconds since epoch
145             final long currentTime = ComponentUtil.getSystemHelper().getCurrentTimeAsLong(); // milliseconds since epoch
146             if (tokenExpiryTime < currentTime) {
147                 if (logger.isDebugEnabled()) {
148                     logger.debug("Token expired: expiryTime={}, currentTime={}", tokenExpiryTime, currentTime);
149                 }
150                 return false;
151             }
152             // Attempt to refresh token using MSAL4J silent authentication
153             try {
154                 final EntraIdAuthenticator authenticator = ComponentUtil.getComponent(EntraIdAuthenticator.class);
155                 final IAuthenticationResult newResult = authenticator.refreshTokenSilently(this);
156                 if (newResult != null) {
157                     authResult = newResult;
158                     authenticator.updateMemberOf(this);
159                     permissions = null;
160                     if (logger.isDebugEnabled()) {
161                         logger.debug("Token refreshed successfully via silent authentication");
162                     }
163                     return true;
164                 }
165             } catch (final Exception e) {
166                 if (logger.isDebugEnabled()) {
167                     logger.debug("Silent token refresh failed: {}", e.getMessage());
168                 }
169             }
170             // For MSAL4J, if silent refresh fails, return true if token is still valid
171             // Actual refresh will happen during next authentication request
172             return true;
173         }
174 
175         /**
176          * Gets the Entra ID authentication result.
177          * @return The authentication result.
178          */
179         public IAuthenticationResult getAuthenticationResult() {
180             return authResult;
181         }
182 
183         /**
184          * Sets the user's group memberships.
185          * @param groups Array of group names.
186          */
187         public synchronized void setGroups(final String[] groups) {
188             this.groups = groups;
189         }
190 
191         /**
192          * Sets the user's role assignments.
193          * @param roles Array of role names.
194          */
195         public synchronized void setRoles(final String[] roles) {
196             this.roles = roles;
197         }
198 
199         /**
200          * Resets permissions to force recalculation on next getPermissions() call.
201          * This is called after asynchronous parent group lookup completes.
202          */
203         public void resetPermissions() {
204             this.permissions = null;
205         }
206     }
207 }