View Javadoc
1   package net.sumaris.server.http.security;
2   
3   /*-
4    * #%L
5    * SUMARiS:: Server
6    * %%
7    * Copyright (C) 2018 SUMARiS Consortium
8    * %%
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   * 
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Public License for more details.
18   * 
19   * You should have received a copy of the GNU General Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/gpl-3.0.html>.
22   * #L%
23   */
24  
25  import net.sumaris.core.dao.administration.user.PersonDao;
26  import net.sumaris.core.dao.administration.user.UserTokenDao;
27  import net.sumaris.core.exception.DataNotFoundException;
28  import net.sumaris.core.model.referential.StatusEnum;
29  import net.sumaris.core.model.referential.UserProfileEnum;
30  import net.sumaris.core.util.crypto.CryptoUtils;
31  import net.sumaris.core.vo.administration.user.PersonVO;
32  import net.sumaris.server.config.SumarisServerConfigurationOption;
33  import net.sumaris.server.service.administration.AccountService;
34  import net.sumaris.server.service.crypto.ServerCryptoService;
35  import net.sumaris.server.vo.security.AuthDataVO;
36  import org.apache.commons.collections.CollectionUtils;
37  import org.apache.commons.lang3.StringUtils;
38  import org.nuiton.i18n.I18n;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  import org.springframework.beans.factory.annotation.Autowired;
42  import org.springframework.core.env.Environment;
43  import org.springframework.dao.DataRetrievalFailureException;
44  import org.springframework.security.core.GrantedAuthority;
45  import org.springframework.security.core.authority.mapping.Attributes2GrantedAuthoritiesMapper;
46  import org.springframework.security.core.authority.mapping.SimpleAttributes2GrantedAuthoritiesMapper;
47  import org.springframework.stereotype.Service;
48  
49  import javax.annotation.PostConstruct;
50  import java.text.ParseException;
51  import java.util.ArrayList;
52  import java.util.List;
53  import java.util.Objects;
54  import java.util.Optional;
55  import java.util.stream.Collectors;
56  
57  @Service("authService")
58  public class AuthServiceImpl implements AuthService {
59  
60      /** Logger. */
61      private static final Logger log =
62              LoggerFactory.getLogger(AuthServiceImpl.class);
63  
64      private final ValidationExpiredCache challenges;
65      private final ValidationExpiredCacheMap<AuthUser> checkedTokens;
66      private final boolean debug;
67  
68      @Autowired
69      private ServerCryptoService cryptoService;
70  
71      @Autowired
72      private AccountService accountService;
73  
74      @Autowired
75      private PersonDao personDao;
76  
77      @Autowired
78      private UserTokenDao userTokenDao;
79  
80      private Attributes2GrantedAuthoritiesMapper authoritiesMapper;
81  
82      @Autowired
83      public AuthServiceImpl(Environment environment) {
84  
85          int challengeLifeTimeInSeconds = Integer.parseInt(environment.getProperty(SumarisServerConfigurationOption.AUTH_CHALLENGE_LIFE_TIME.getKey(), SumarisServerConfigurationOption.AUTH_CHALLENGE_LIFE_TIME.getDefaultValue()));
86          this.challenges = new ValidationExpiredCache(challengeLifeTimeInSeconds);
87  
88          int tokenLifeTimeInSeconds = Integer.parseInt(environment.getProperty(SumarisServerConfigurationOption.AUTH_TOKEN_LIFE_TIME.getKey(), SumarisServerConfigurationOption.AUTH_TOKEN_LIFE_TIME.getDefaultValue()));
89          this.checkedTokens = new ValidationExpiredCacheMap<>(tokenLifeTimeInSeconds);
90  
91          authoritiesMapper = new SimpleAttributes2GrantedAuthoritiesMapper();
92  
93          this.debug = log.isDebugEnabled();
94      }
95  
96      @PostConstruct
97      public void registerListeners() {
98          // Listen person update, to update the cache
99          personDao.addListener(new PersonDao.Listener() {
100             @Override
101             public void onSave(PersonVO person) {
102                 if (!StringUtils.isNotBlank(person.getPubkey())) return;
103                 List<String> tokens = userTokenDao.getAllByPubkey(person.getPubkey());
104                 if (CollectionUtils.isEmpty(tokens)) return;
105                 tokens.forEach(checkedTokens::remove);
106             }
107 
108             @Override
109             public void onDelete(int id) {
110                 // Will be remove when cache expired
111             }
112         });
113     }
114 
115     @Override
116     public Optional<AuthUser> authenticate(String token) {
117 
118         // First check anonymous user
119         if (AnonymousUser.TOKEN.equals(token)) return Optional.of(AnonymousUser.INSTANCE);
120 
121         // Check if present in cache
122         if (checkedTokens.contains(token)) return Optional.of(checkedTokens.get(token));
123 
124         // Parse the token
125         AuthDataVO authData;
126         try {
127             authData = AuthDataVO.parse(token);
128         } catch(ParseException e) {
129             log.warn("Authentication failed. Invalid token: " + token);
130             return Optional.empty();
131         }
132 
133         // Try to authenticate
134         AuthUser authUser = authenticate(authData);
135 
136         return Optional.ofNullable(authUser);
137     }
138 
139     private AuthUser authenticate(AuthDataVO authData) {
140 
141         // Check if pubkey can authenticate
142         try {
143             if (authData.getPubkey().length() < 6) {
144                 if (debug) log.debug("Authentication failed. Bad pubkey format: " + authData.getPubkey());
145                 return null;
146             }
147             if (!canAuth(authData.getPubkey())) {
148                 if (debug) log.debug("Authentication failed. User is not allowed to authenticate: " + authData.getPubkey());
149                 return null;
150             }
151         } catch(DataNotFoundException | DataRetrievalFailureException e) {
152             log.debug("Authentication failed. User not found: " + authData.getPubkey());
153             return null;
154         }
155 
156         // Token exists on database: check as new challenge response
157         boolean isStoredToken = accountService.isStoredToken(authData.asToken(), authData.getPubkey());
158         if (!isStoredToken) {
159             log.debug("Unknown token. Check if response to new challenge...");
160 
161             // Make sure the challenge exists and not expired
162             if (!challenges.contains(authData.getChallenge())) {
163                 if (debug)
164                     log.debug("Authentication failed. Challenge not found or expired: " + authData.getChallenge());
165                 return null;
166             }
167         }
168 
169         // Check signature
170         if (!cryptoService.verify(authData.getChallenge(), authData.getSignature(), authData.getPubkey())) {
171             if (debug) log.debug("Authentication failed. Bad challenge signature in token: " + authData.toString());
172             return null;
173         }
174 
175         // Auth success !
176 
177         // Force challenge to expire
178         challenges.remove(authData.getChallenge());
179 
180         // Get authorities
181         List<GrantedAuthority> authorities = getAuthorities(authData.getPubkey());
182 
183         // Create authenticated user
184         AuthUserity/AuthUser.html#AuthUser">AuthUser authUser = new AuthUser(authData, authorities);
185 
186         // Add token to store
187         String token = authData.toString();
188         checkedTokens.add(token, authUser);
189 
190         if(!isStoredToken) {
191             // Save this new token to database
192             try {
193                 accountService.addToken(token, authData.getPubkey());
194             } catch (RuntimeException e) {
195                 // Log then continue
196                 log.error("Could not save auth token.", e);
197             }
198         }
199 
200         if (debug) log.debug(String.format("Authentication succeed for user with pubkey {%s}", authData.getPubkey().substring(0, 6)));
201 
202         return authUser;
203     }
204 
205     private List<GrantedAuthority> getAuthorities(String pubkey) {
206         List<Integer> profileIds = accountService.getProfileIdsByPubkey(pubkey);
207 
208         return new ArrayList<>(authoritiesMapper.getGrantedAuthorities(profileIds.stream()
209                 .map(id -> UserProfileEnum.getLabelById(id).orElse(null))
210                 .filter(Objects::nonNull)
211                 .collect(Collectors.toSet())));
212 
213     }
214 
215     private boolean canAuth(final String pubkey) throws DataNotFoundException {
216         PersonVO person = personDao.getByPubkeyOrNull(pubkey);
217         if (person == null) {
218             throw new DataRetrievalFailureException(I18n.t("sumaris.error.account.notFound"));
219         }
220 
221         // Cannot auth if user has been deleted or is disable
222         StatusEnum status = StatusEnum.valueOf(person.getStatusId());
223         if (StatusEnum.DISABLE.equals(status) || StatusEnum.DELETED.equals(status)) {
224             return false;
225         }
226 
227         // TODO: check if necessary ?
228         /*
229         List<Integer> userProfileIds = accountService.getProfileIdsByPubkey(pubkey);
230         boolean result = CollectionUtils.containsAny(userProfileIds, AUTH_ACCEPTED_PROFILES);
231         if (debug) log.debug(String.format("User with pubkey {%s} %s authenticate, because he has this profiles: %s", pubkey.substring(0,6), (result ? "can" : "cannot"), userProfileIds));
232         return result;
233         */
234 
235         return true;
236     }
237 
238     public AuthDataVO createNewChallenge() {
239         String challenge = newChallenge();
240         String signature = cryptoService.sign(challenge);
241 
242         AuthDataVOy/AuthDataVO.html#AuthDataVO">AuthDataVO result = new AuthDataVO(cryptoService.getServerPubkey(), challenge, signature);
243 
244         if (debug) log.debug("New authentication challenge: " + result.toString());
245 
246         // Add challenge to cache
247         challenges.add(challenge);
248 
249         return result;
250     }
251 
252     /* -- new challenge -- */
253 
254     private String newChallenge() {
255         byte[] randomNonce = cryptoService.getBoxRandomNonce();
256         String randomNonceStr = CryptoUtils.encodeBase64(randomNonce);
257         return cryptoService.hash(randomNonceStr);
258     }
259 }