PageRenderTime 44ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/src/main/java/org/integratedmodelling/node/service/AuthService.java

https://bitbucket.org/jlukescott/klab-node
Java | 660 lines | 406 code | 89 blank | 165 comment | 44 complexity | a460502efb758a26a9a69e794b024095 MD5 | raw file
  1. /**
  2. * - BEGIN LICENSE: 4552165799761088680 -
  3. *
  4. * Copyright (C) 2014-2018 by:
  5. * - J. Luke Scott <luke@cron.works>
  6. * - Ferdinando Villa <ferdinando.villa@bc3research.org>
  7. * - any other authors listed in the @author annotations in source files
  8. *
  9. * This program is free software; you can redistribute it and/or
  10. * modify it under the terms of the Affero General Public License
  11. * Version 3 or any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the Affero General Public License
  19. * along with this program; if not, write to the Free Software
  20. * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  21. * The license is also available at: https://www.gnu.org/licenses/agpl.html
  22. *
  23. * - END LICENSE -
  24. */
  25. package org.integratedmodelling.node.service;
  26. import java.io.IOException;
  27. import java.lang.reflect.Constructor;
  28. import java.net.MalformedURLException;
  29. import java.time.ZonedDateTime;
  30. import java.util.ArrayList;
  31. import java.util.Arrays;
  32. import java.util.Collection;
  33. import java.util.HashMap;
  34. import java.util.List;
  35. import java.util.Map;
  36. import java.util.Map.Entry;
  37. import javax.annotation.PostConstruct;
  38. import javax.mail.MessagingException;
  39. import org.apache.commons.lang3.RandomStringUtils;
  40. import org.hibernate.validator.internal.constraintvalidators.hv.EmailValidator;
  41. import org.integratedmodelling.node.auth.AuthenticationToken;
  42. import org.integratedmodelling.node.auth.Role;
  43. import org.integratedmodelling.node.auth.VerifyEmailClickbackToken;
  44. import org.integratedmodelling.node.config.CoreApplicationConfigProd;
  45. import org.integratedmodelling.node.config.WebSecurityConfig;
  46. import org.integratedmodelling.node.domain.ProfileResource;
  47. import org.integratedmodelling.node.domain.UserData;
  48. import org.integratedmodelling.node.domain.UserData.AccountStatus;
  49. import org.integratedmodelling.node.exception.AuthenticationFailedException;
  50. import org.integratedmodelling.node.exception.BadRequestException;
  51. import org.integratedmodelling.node.exception.JwksNotFoundException;
  52. import org.integratedmodelling.node.exception.JwtExpiredException;
  53. import org.integratedmodelling.node.exception.TokenGenerationException;
  54. import org.integratedmodelling.node.exception.UnauthorizedException;
  55. import org.integratedmodelling.node.exception.UserEmailExistsException;
  56. import org.integratedmodelling.node.exception.UserExistsException;
  57. import org.integratedmodelling.node.resource.token.ActivateAccountClickbackToken;
  58. import org.integratedmodelling.node.resource.token.ChangePasswordClickbackToken;
  59. import org.integratedmodelling.node.resource.token.ClickbackToken;
  60. import org.integratedmodelling.node.util.DateTimeUtil;
  61. import org.jose4j.jwk.HttpsJwks;
  62. import org.jose4j.jws.AlgorithmIdentifiers;
  63. import org.jose4j.jws.JsonWebSignature;
  64. import org.jose4j.jwt.JwtClaims;
  65. import org.jose4j.jwt.MalformedClaimException;
  66. import org.jose4j.jwt.NumericDate;
  67. import org.jose4j.jwt.consumer.InvalidJwtException;
  68. import org.jose4j.jwt.consumer.JwtConsumer;
  69. import org.jose4j.jwt.consumer.JwtConsumerBuilder;
  70. import org.jose4j.jwt.consumer.JwtContext;
  71. import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver;
  72. import org.jose4j.lang.JoseException;
  73. import org.springframework.beans.factory.annotation.Autowired;
  74. import org.springframework.context.annotation.Profile;
  75. import org.springframework.scheduling.annotation.Scheduled;
  76. import org.springframework.security.authentication.AuthenticationManager;
  77. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  78. import org.springframework.security.core.Authentication;
  79. import org.springframework.security.core.AuthenticationException;
  80. import org.springframework.security.core.GrantedAuthority;
  81. import org.springframework.security.core.context.SecurityContextHolder;
  82. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  83. import org.springframework.stereotype.Component;
  84. @Component
  85. @Profile({ CoreApplicationConfigProd.CONFIG_PROFILE })
  86. public class AuthService extends Service {
  87. private static final int ALLOWED_CLOCK_SKEW_MS = 30000;
  88. private static final String DEFAULT_TOKEN_CLASS = AuthenticationToken.class.getSimpleName();
  89. private static final long JWKS_UPDATE_INTERVAL_MS = 10 * 60 * 1000; // every 10 minutes
  90. private static final String JWT_CLAIM_KEY_PERMISSIONS = "perms";
  91. private static final String JWT_CLAIM_TOKEN_TYPE = "cls";
  92. private static final String JWT_DEFAULT_AUDIENCE = "all";
  93. @SuppressWarnings("unchecked")
  94. private static final Class<Collection<Role>> ROLE_COLLECTION_CLASS = (Class<Collection<Role>>) (Class<?>) Collection.class;
  95. @Autowired
  96. private AuthenticationManager authenticationManager;
  97. @Autowired
  98. private DateTimeUtil dateTimeUtil;
  99. @Autowired
  100. private EmailService emailManager;
  101. private EmailValidator emailValidator = new EmailValidator();
  102. protected Map<String, HttpsJwks> jwksUpdaters;
  103. private Map<String, JwtConsumer> jwksVerifiers;
  104. private JwtConsumer preValidationExtractor;
  105. @Autowired
  106. private KLabUserDetailsService userDetailsManager;
  107. @Autowired
  108. protected WebSecurityConfig webSecurityConfig;
  109. public void activateAccount() {
  110. // the user is already authenticated via clickback token header, but we want
  111. // to do some extra verification because they aren't supplying a password
  112. Authentication authentication = securityHelper.getLoggedInAuthentication();
  113. if (!(authentication instanceof ActivateAccountClickbackToken)) {
  114. throw new AuthenticationFailedException("The token submitted was not valid for activating an account.");
  115. }
  116. // this will also verify that the account started in pendingActivation
  117. userDetailsManager.activateUser(securityHelper.getLoggedInUsername());
  118. }
  119. // public UserData addUserToGroups(String username, List<String> groups) {
  120. // UserData userData = securityHelper.getUserData(username);
  121. // userData.addGroups(groups);
  122. // userDetailsManager.updateUser(userData);
  123. // return userData;
  124. // }
  125. public AuthenticationToken authenticate(String username, String password) {
  126. AuthenticationToken result = null;
  127. username = dataUtil.cleanseUsername(username);
  128. try {
  129. // NOTE: this is not created with createAuthenticationToken() because it's not saved to the DB
  130. Authentication authRequest = new UsernamePasswordAuthenticationToken(username, password);
  131. Authentication authResult = authenticationManager.authenticate(authRequest);
  132. logger.info(username, "Authenticated user via password authentication " + authResult.getAuthorities(),
  133. null);
  134. if (!authResult.isAuthenticated()) {
  135. String msg = "Something went wrong with authentication. Result.isAuthenticated() == false, but no exception was thrown.";
  136. logger.error(msg + " Throwing now...");
  137. throw new AuthenticationException(msg) {
  138. private static final long serialVersionUID = -6920183737495378174L;
  139. };
  140. } else {
  141. if (emailValidator.isValid(username, null)) {
  142. // if the user isn't in Mongo yet, but it appears that the username is an email address,
  143. // then it's OK to auto-create the user with username == email.
  144. // NOTE: This is valid only because they have successfully authenticated above (i.e.
  145. // they can be assumed to be "approved" by LDAP or whatever component allowed .authenticate())
  146. createMongoUserIfNecessary(username, username);
  147. }
  148. result = createAuthenticationToken(username, AuthenticationToken.class);
  149. // register the authentication with Spring Security
  150. SecurityContextHolder.getContext().setAuthentication(result);
  151. updateLastLogin(username);
  152. }
  153. } catch (AuthenticationException e) {
  154. String msg = "Login failed for user: " + username;
  155. logger.error(username, msg, e);
  156. throw new AuthenticationFailedException(msg, e);
  157. }
  158. return result;
  159. }
  160. // public AuthenticationToken authenticateWithCertFile(String certFileContent) throws IOException, PGPException {
  161. // Properties properties = licenseManager.readCertFileContent(certFileContent);
  162. // String username = properties.getProperty(LicenseService.Fields.USER);
  163. // logger.info(String.format("Attempting to authenticate user %s via cert file...", username));
  164. //
  165. // String expiryString = properties.getProperty(LicenseService.Fields.EXPIRY);
  166. //
  167. // DateTime expiry = DateTime.parse(expiryString);
  168. // if (expiry.isBeforeNow()) {
  169. // String msg = String.format("The cert file submitted for user %s is expired.", username);
  170. // logger.error(msg);
  171. // throw new AuthenticationFailedException(msg);
  172. // }
  173. //
  174. // logger.info(
  175. // String.format("Successfully authenticated user %s via cert file. Generating EngineToken...", username));
  176. //
  177. // AuthenticationToken result = createAuthenticationToken(username, EngineToken.class);
  178. // updateLastEngineConnection(username);
  179. // return result;
  180. // }
  181. /**
  182. * By default, we just create an instance of the jose4j /jwks retriever.
  183. *
  184. * Override this method to provide a mock object for unit tests, so that we don't require any network communication.
  185. */
  186. protected HttpsJwks buildJwksClient(String url) {
  187. return new HttpsJwks(url);
  188. }
  189. /**
  190. * The user is logged in via username/password
  191. */
  192. // public void changePassword(String oldPassword, String newPassword) {
  193. // String username = securityHelper.getLoggedInUsername();
  194. // UsernamePasswordAuthenticationToken usernamePassword = new UsernamePasswordAuthenticationToken(username,
  195. // oldPassword);
  196. // Authentication authResult = authenticationManager.authenticate(usernamePassword);
  197. // if (!authResult.isAuthenticated()) {
  198. // String msg = String.format("Old password was incorrect while trying password change for user '%s'.",
  199. // username);
  200. // logger.error(msg);
  201. // throw new AuthenticationFailedException(msg);
  202. // }
  203. //
  204. // setPasswordAndSendVerificationEmail(newPassword);
  205. // }
  206. /**
  207. * ALL tokens should be created with these two methods
  208. */
  209. public AuthenticationToken createAuthenticationToken(String username,
  210. Class<? extends AuthenticationToken> tokenType) {
  211. if (ClickbackToken.class.isAssignableFrom(tokenType)) {
  212. throw new TokenGenerationException(
  213. "ClickbackTokens must be generated by createClickbackToken(), not createAuthenticationToken()",
  214. null);
  215. }
  216. AuthenticationToken result = null;
  217. username = dataUtil.cleanseUsername(username);
  218. UserData userData = userDetailsManager.loadUserByUsername(username);
  219. Collection<Role> userAuthorities = userData.getAuthorities();
  220. Constructor<? extends AuthenticationToken> constructor = null;
  221. try {
  222. constructor = tokenType.getConstructor(String.class, String.class, ROLE_COLLECTION_CLASS);
  223. result = constructor.newInstance(coreApplicationConfig.getPartnerId(), username, userAuthorities);
  224. } catch (Exception e) {
  225. try {
  226. // probably a token type that doesn't accept roles in the constructor (cronjob/engine/etc).
  227. // re-try with just partner ID + username
  228. constructor = tokenType.getConstructor(String.class, String.class, ROLE_COLLECTION_CLASS);
  229. result = constructor.newInstance(coreApplicationConfig.getPartnerId(), username);
  230. } catch (Exception e2) {
  231. // shouldn't ever get here, but if we do, wrap in a non-checked Exception type
  232. throw new TokenGenerationException("Unable to get token constructor method.", e2);
  233. }
  234. }
  235. // In case the token type sets its own roles (i.e. no roles parameter in the constructor),
  236. // we need to double check the user has permissions to generate tokens with all included roles
  237. // (this is a no-op if the constructor accepted a roles parameter)
  238. for (GrantedAuthority authority : result.getAuthorities()) {
  239. if (!userAuthorities.contains(authority)) {
  240. throw new BadRequestException(String.format(
  241. "User '%s' does not have required role '%s' for this token type.", username, authority));
  242. }
  243. }
  244. result.setAuthenticated(true);
  245. result.setTokenString(generateJwtString(result));
  246. return result;
  247. }
  248. /**
  249. * ALL tokens should be created with these two methods
  250. */
  251. public ClickbackToken createClickbackToken(String username, Class<? extends ClickbackToken> tokenType)
  252. throws MalformedURLException {
  253. ClickbackToken result = null;
  254. username = dataUtil.cleanseUsername(username);
  255. try {
  256. result = tokenType.getConstructor(String.class, String.class)
  257. .newInstance(coreApplicationConfig.getPartnerId(), username);
  258. } catch (Exception e) {
  259. // mainly this is to throw RuntimeException instead of a checked exception
  260. // (shouldn't get here though)
  261. throw new TokenGenerationException("Unable to get token constructor method.", e);
  262. }
  263. result.setClickbackUrl(coreApplicationConfig);
  264. result.setAuthenticated(true);
  265. result.setTokenString(generateJwtString(result));
  266. // TODO save clickbacks? (payload too big for JWT?)
  267. // tokenRepository.save(result);
  268. return result;
  269. }
  270. private void createMongoUser(String username, String email) {
  271. UserData userData;
  272. userData = new UserData();
  273. userData.setUsername(username);
  274. userData.setEmail(email);
  275. userData.setRoles(Arrays.asList(Role.ROLE_USER));
  276. userData.setRegistrationDate();
  277. userDetailsManager.createMongoUser(userData);
  278. }
  279. private void createMongoUserIfNecessary(String username, String email) {
  280. try {
  281. createMongoUser(username, email);
  282. } catch (UserExistsException | UserEmailExistsException e) {
  283. // OK to continue silently
  284. }
  285. }
  286. public void createNewUser(String username, String email) throws TokenGenerationException, MalformedURLException {
  287. UserData userData = null;
  288. try {
  289. userData = userDetailsManager.loadUserByUsername(username);
  290. } catch (UsernameNotFoundException e) {
  291. }
  292. if (userData != null) {
  293. if (!AccountStatus.pendingActivation.equals(userData.getAccountStatus())
  294. || !email.equals(userData.getEmail())) {
  295. // an existing account is in Mongo with either a mis-matched email or is not in 'pendingActivation' state
  296. // so don't let the user re-register. (Active users without LDAP records can 'reset password' instead)
  297. throw new BadRequestException("An account with this username already exists.\n"
  298. + "If you are re-sending a registration email, please use the same email address as before.");
  299. }
  300. // the existing user account is pendingActivation and the email matches,
  301. // so it's OK to register as if it's a new account
  302. } else {
  303. // user is brand new
  304. try {
  305. createMongoUser(username, email);
  306. } catch (UserExistsException | UserEmailExistsException e) {
  307. throw new BadRequestException(e.getMessage(), e);
  308. }
  309. }
  310. ClickbackToken clickbackToken = createClickbackToken(username, ActivateAccountClickbackToken.class);
  311. emailManager.sendNewUser(email, username, clickbackToken.getClickbackUrl());
  312. }
  313. /**
  314. * email-verification-specific wrapper for createClickbackToken()
  315. */
  316. public VerifyEmailClickbackToken createVerifyEmailClickbackToken(String username, String email)
  317. throws MalformedURLException {
  318. VerifyEmailClickbackToken token = (VerifyEmailClickbackToken) createClickbackToken(username,
  319. VerifyEmailClickbackToken.class);
  320. token.setNewEmailAddress(email);
  321. // TODO need to save it somehow, I think, because this is too much data for JWT (right?)
  322. // tokenRepository.save(token);
  323. return token;
  324. }
  325. public void decorate(UserData userData) throws MalformedURLException {
  326. if (userData.getServerUrl() == null) {
  327. userData.setServerUrl(coreApplicationConfig.getEngineUrl().toExternalForm());
  328. }
  329. }
  330. private String generateJwtString(AuthenticationToken token) {
  331. if (!token.isAuthenticated()) {
  332. return null;
  333. }
  334. JwtClaims claims = new JwtClaims();
  335. String tokenClass = token.getClass().getSimpleName();
  336. if (!tokenClass.equals(DEFAULT_TOKEN_CLASS)) {
  337. claims.setStringClaim(JWT_CLAIM_TOKEN_TYPE, tokenClass);
  338. }
  339. claims.setIssuer(webSecurityConfig.getIssuer());
  340. claims.setAudience(Arrays.asList(JWT_DEFAULT_AUDIENCE));
  341. claims.setSubject(token.getUsername());
  342. ZonedDateTime expirationTimeUtc = dateTimeUtil.localToUtc(token.getExpiration());
  343. NumericDate expirationTimeNumericDate = NumericDate.fromSeconds(expirationTimeUtc.toEpochSecond());
  344. claims.setExpirationTime(expirationTimeNumericDate);
  345. claims.setIssuedAt(NumericDate.now());
  346. claims.setGeneratedJwtId();
  347. List<String> roleStrings = new ArrayList<>();
  348. for (Role role : token.getRoles()) {
  349. roleStrings.add(role.name());
  350. }
  351. claims.setStringListClaim(JWT_CLAIM_KEY_PERMISSIONS, roleStrings);
  352. JsonWebSignature jws = new JsonWebSignature();
  353. jws.setPayload(claims.toJson());
  354. jws.setKey(webSecurityConfig.getJwtPrivateKey());
  355. jws.setKeyIdHeaderValue(webSecurityConfig.getJwtPrivateKeyId());
  356. jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
  357. String result;
  358. try {
  359. result = jws.getCompactSerialization();
  360. } catch (JoseException e) {
  361. result = null;
  362. logger.error(String.format("Failed to generate JWT token string for user '%s': ", token.getUsername()), e);
  363. }
  364. return result;
  365. }
  366. public ClickbackToken handleClickbackToken(String tokenString) {
  367. // TODO need to save it somehow, I think, because this is too much data for JWT (right?)
  368. // ClickbackToken token = (ClickbackToken) tokenRepository.findByTokenString(tokenString);
  369. // if (token == null) {
  370. // throw new ForbiddenException("The token submitted could not be found.");
  371. // }
  372. // token.handleClickback(this);
  373. // return token;
  374. return null;
  375. }
  376. @PostConstruct
  377. public void postConstruct() throws JoseException, IOException {
  378. preValidationExtractor = new JwtConsumerBuilder().setSkipAllValidators().setDisableRequireSignature()
  379. .setSkipSignatureVerification().build();
  380. jwksUpdaters = new HashMap<>();
  381. jwksVerifiers = new HashMap<>();
  382. for (Entry<String, String> entry : webSecurityConfig.getJwksEndpointsByIssuer().entrySet()) {
  383. HttpsJwks jwks = buildJwksClient(entry.getValue());
  384. HttpsJwksVerificationKeyResolver jwksKeyResolver = new HttpsJwksVerificationKeyResolver(jwks);
  385. JwtConsumer jwtVerifier = new JwtConsumerBuilder().setSkipDefaultAudienceValidation() // audience == Client ID, which will be checked against the policy later.
  386. .setAllowedClockSkewInSeconds(ALLOWED_CLOCK_SKEW_MS / 1000)
  387. .setVerificationKeyResolver(jwksKeyResolver).build();
  388. jwksUpdaters.put(entry.getKey(), jwks);
  389. jwksVerifiers.put(entry.getKey(), jwtVerifier);
  390. }
  391. }
  392. public void sendPasswordResetEmail(String username, String email, boolean invalidateCurrentPassword)
  393. throws MessagingException, MalformedURLException {
  394. UserData user = userDetailsManager.loadUserByUsername(username);
  395. if (email == null || !email.equals(user.getEmail())) {
  396. throw new AuthenticationFailedException("Could not find a user with that username and email address.");
  397. }
  398. if (invalidateCurrentPassword) {
  399. userDetailsManager.changePasswordForUser(username, RandomStringUtils.randomAlphanumeric(15));
  400. }
  401. ClickbackToken token = createClickbackToken(username, ChangePasswordClickbackToken.class);
  402. emailManager.sendPasswordReset(email, token.getClickbackUrl());
  403. }
  404. public void setEmailWithTokenVerification(String newEmail) {
  405. // The user will have logged in via PasswordChangeClickbackToken or username/password.
  406. UserData persistedUser = securityHelper.getLoggedInUserData();
  407. if (persistedUser == null) {
  408. throw new BadRequestException("Could not find a user with the token that was submitted.");
  409. } else if (!AccountStatus.active.equals(persistedUser.getAccountStatus())) {
  410. throw new BadRequestException("An active user could not be found with the token that was submitted.");
  411. } else {
  412. // String oldEmail = persistedUser.getEmail();
  413. persistedUser.setEmail(newEmail);
  414. userDetailsManager.updateUser(persistedUser);
  415. // TODO
  416. // emailManager.sendEmailChangeConfirmation(oldEmail, persistedUser.getUsername());
  417. }
  418. }
  419. private void setPasswordAndSendVerificationEmail(String newPassword) {
  420. // The user will have logged in via PasswordChangeClickbackToken or username/password.
  421. UserData persistedUser = securityHelper.getLoggedInUserData();
  422. if (persistedUser == null) {
  423. throw new BadRequestException("Could not find a user with the token that was submitted.");
  424. } else if (!AccountStatus.active.equals(persistedUser.getAccountStatus())) {
  425. throw new BadRequestException("An active user could not be found with the token that was submitted.");
  426. } else {
  427. // in case the user is changing their password to solve a broken state (i.e. Mongo but no LDAP)
  428. // this will prevent the missing LDAP record from breaking the process
  429. if (!userDetailsManager.springUserExists(persistedUser.getUsername())) {
  430. userDetailsManager.createSpringUser(persistedUser);
  431. }
  432. userDetailsManager.changePassword(null, newPassword);
  433. // send an email notifying the user their password was changed
  434. // TODO
  435. // emailManager.sendPasswordChangeConfirmation(persistedUser.getEmail());
  436. }
  437. }
  438. /**
  439. * The user is logged in by ActivateAccountClickbackToken or ChangePasswordClickbackToken,
  440. * and their account should be 'active' by now.
  441. */
  442. public void setPasswordWithTokenVerification(String newPassword) {
  443. // the user is already authenticated via clickback token header, but we want
  444. // to do some extra verification because they aren't supplying an old password
  445. Authentication authentication = securityHelper.getLoggedInAuthentication();
  446. if (!(authentication instanceof ChangePasswordClickbackToken)) {
  447. throw new AuthenticationFailedException("The token submitted was not valid for setting a new password.");
  448. }
  449. setPasswordAndSendVerificationEmail(newPassword);
  450. // TODO save & delete clickback tokens, right?
  451. // manually delete the token because the normal clickback mechanism won't be doing it
  452. // deleteToken(((ChangePasswordClickbackToken) authentication).getTokenString());
  453. }
  454. @Scheduled(fixedDelay = JWKS_UPDATE_INTERVAL_MS)
  455. public void updateKeys() throws IOException {
  456. for (HttpsJwks jwks : jwksUpdaters.values()) {
  457. try {
  458. jwks.refresh();
  459. } catch (JoseException e) {
  460. logger.error("Got exception while refreshing JKW key set from " + jwks.getLocation(), e);
  461. }
  462. }
  463. }
  464. private void updateLastLogin(String username) {
  465. UserData userData = userDetailsManager.loadUserByUsername(username);
  466. userData.setLastLogin();
  467. userDetailsManager.updateUser(userData);
  468. }
  469. public ProfileResource updateProfile(ProfileResource profileResource) throws MalformedURLException {
  470. UserData loggedInUser = securityHelper.getLoggedInUserData();
  471. dataUtil.cleanse(profileResource);
  472. UserData userData;
  473. logger.info("Attempting to update profile for user: " + profileResource.username);
  474. if (loggedInUser.getUsername().equals(profileResource.username)) {
  475. if (!loggedInUser.isAdmin()) {
  476. // a normal user is modifying him/herself. Enforce security rules.
  477. profileResource.accountStatus = loggedInUser.getAccountStatus();
  478. profileResource.groups = new ArrayList<>(loggedInUser.getGroups());
  479. profileResource.roles = new ArrayList<>(loggedInUser.getAuthorities());
  480. }
  481. userData = loggedInUser;
  482. // email changes must be done via email-verify clickback
  483. String existingEmail = userData.getEmail();
  484. if (!profileResource.email.equals(existingEmail)) {
  485. VerifyEmailClickbackToken token = createVerifyEmailClickbackToken(profileResource.username,
  486. profileResource.email);
  487. emailManager.sendVerifyEmailClickback(existingEmail, token.getClickbackUrl());
  488. // don't actually change it yet
  489. profileResource.email = existingEmail;
  490. }
  491. } else {
  492. // the user is modifying someone else. Make sure that's ok.
  493. // this should be logged by Spring
  494. if (!loggedInUser.isAdmin()) {
  495. throw new UnauthorizedException(
  496. String.format("The currently logged in user does not have access to modify user '%s'.",
  497. profileResource.username));
  498. }
  499. // throws if not found, meaning that an existing record is required.
  500. // (because 'create' is a very special thing)
  501. userData = securityHelper.getUserData(profileResource.username);
  502. }
  503. // NOTE: at this point, we know that either:
  504. // - user is modifying him/herself and groups are NOT changing
  505. // - user is an admin modifying someone else and groups MAY BE changing.
  506. userData.setFromProfileResource(profileResource);
  507. userDetailsManager.updateUser(userData);
  508. // re-apply anything that may have resulted from setFromProfileResource() (like filtering/merging/etc)
  509. profileResource = objectMapper.convertValue(userData, ProfileResource.class);
  510. return profileResource;
  511. }
  512. /**
  513. * Given a JWT token that has previously been generated by a login event, validate its payload & signature.
  514. * If it passes all checks and its payload can be extracted properly, then return
  515. * an AuthenticationToken representing it.
  516. */
  517. public AuthenticationToken validateJwt(String token) {
  518. AuthenticationToken result = new AuthenticationToken("UNKNOWN", "UNKNOWN");
  519. try {
  520. // first extract the partnerId so that we know which public key to use for validating the signature
  521. JwtContext jwtContext = preValidationExtractor.process(token);
  522. String partnerId = jwtContext.getJwtClaims().getIssuer().trim();
  523. JwtConsumer jwtVerifier = jwksVerifiers.get(partnerId);
  524. if (jwtVerifier == null) {
  525. String msg = String.format("Couldn't find JWT verifier for partnerId %s. I only know about %s.",
  526. partnerId, jwksVerifiers.keySet().toString());
  527. Exception e = new JwksNotFoundException(msg);
  528. logger.error(msg, e);
  529. throw e;
  530. }
  531. JwtClaims claims = jwtVerifier.processToClaims(token);
  532. String username = claims.getSubject();
  533. List<String> roleStrings = claims.getStringListClaimValue(JWT_CLAIM_KEY_PERMISSIONS);
  534. List<Role> roles = new ArrayList<>();
  535. for (String role : roleStrings) {
  536. roles.add(Role.valueOf(role));
  537. }
  538. // didn't throw an exception, so token is valid. Update the result and validate claims.
  539. result = new AuthenticationToken(partnerId, username, roles);
  540. // TODO should we filter by audience somehow? Maybe for "modeling" vs. "web" operations?
  541. // Audience (aud) - The "aud" (audience) claim identifies the recipients that the JWT is intended for.
  542. // Each principal intended to process the JWT must identify itself with a value in the audience claim.
  543. // If the principal processing the claim does not identify itself with a value in the aud claim
  544. // when this claim is present, then the JWT must be rejected.
  545. claims.getAudience();
  546. // Expiration time (exp) - The "exp" (expiration time) claim identifies the expiration time
  547. // on or after which the JWT must not be accepted for processing.
  548. // The value should be in NumericDate[10][11] format.
  549. NumericDate expirationTime = claims.getExpirationTime();
  550. long now = System.currentTimeMillis();
  551. if (expirationTime.isBefore(NumericDate.fromMilliseconds(now - ALLOWED_CLOCK_SKEW_MS))) {
  552. throw new JwtExpiredException(claims);
  553. }
  554. long issuedAtUtcMs = claims.getIssuedAt().getValueInMillis();
  555. ZonedDateTime issuedAt = dateTimeUtil.utcToLocal(issuedAtUtcMs);
  556. result.setIssuedAt(issuedAt);
  557. // TODO if claims validate, then mark the result as authenticated
  558. result.setAuthenticated(true);
  559. } catch (MalformedClaimException e) {
  560. // TODO
  561. logger.error("WTF", e);
  562. } catch (InvalidJwtException e) {
  563. // TODO
  564. logger.error("WTF", e);
  565. } catch (Exception e) {
  566. // it was a JWT token, but some other exception happened.
  567. logger.error("WTF", e);
  568. }
  569. return result;
  570. }
  571. }