PageRenderTime 82ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/org.amdatu.security.account/src/org/amdatu/security/account/admin/AccountAdminImpl.java

https://bitbucket.org/amdatu/amdatu-security
Java | 500 lines | 346 code | 100 blank | 54 comment | 31 complexity | e0460bd62abe91ed9f9967320f9082c4 MD5 | raw file
  1. /*
  2. * Licensed under the Apache License, Version 2.0 (the "License");
  3. * you may not use this file except in compliance with the License.
  4. * You may obtain a copy of the License at
  5. *
  6. * http://www.apache.org/licenses/LICENSE-2.0
  7. *
  8. * Unless required by applicable law or agreed to in writing, software
  9. * distributed under the License is distributed on an "AS IS" BASIS,
  10. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. * See the License for the specific language governing permissions and
  12. * limitations under the License.
  13. */
  14. package org.amdatu.security.account.admin;
  15. import static java.util.stream.Collectors.toMap;
  16. import static org.amdatu.security.account.AccountConstants.KEY_ACCOUNT_ACCESS_TOKEN;
  17. import static org.amdatu.security.account.AccountConstants.KEY_ACCOUNT_CREDENTIALS;
  18. import static org.amdatu.security.account.AccountConstants.KEY_ACCOUNT_ID;
  19. import static org.amdatu.security.account.AccountConstants.KEY_ACCOUNT_STATE;
  20. import static org.amdatu.security.account.AccountConstants.TOPIC_ACCOUNT_CREATED;
  21. import static org.amdatu.security.account.AccountConstants.TOPIC_ACCOUNT_REMOVED;
  22. import static org.amdatu.security.account.AccountConstants.TOPIC_ACCOUNT_RESET_CREDENTIALS;
  23. import static org.amdatu.security.account.AccountConstants.TOPIC_ACCOUNT_UPDATED;
  24. import static org.amdatu.security.tokenprovider.TokenConstants.EXPIRATION_TIME;
  25. import static org.amdatu.security.tokenprovider.TokenConstants.NOT_BEFORE;
  26. import static org.amdatu.security.tokenprovider.TokenConstants.SUBJECT;
  27. import static org.osgi.service.log.LogService.LOG_DEBUG;
  28. import static org.osgi.service.log.LogService.LOG_INFO;
  29. import static org.osgi.service.log.LogService.LOG_WARNING;
  30. import java.time.Instant;
  31. import java.time.temporal.ChronoUnit;
  32. import java.util.Dictionary;
  33. import java.util.HashMap;
  34. import java.util.Map;
  35. import java.util.Objects;
  36. import java.util.Optional;
  37. import java.util.Set;
  38. import org.amdatu.security.account.Account;
  39. import org.amdatu.security.account.Account.State;
  40. import org.amdatu.security.account.AccountAdmin;
  41. import org.amdatu.security.account.AccountAdminBackend;
  42. import org.amdatu.security.account.AccountCredentialResetException;
  43. import org.amdatu.security.account.AccountException;
  44. import org.amdatu.security.account.AccountExistsException;
  45. import org.amdatu.security.account.AccountLockedException;
  46. import org.amdatu.security.account.AccountValidator;
  47. import org.amdatu.security.account.NoSuchAccountException;
  48. import org.amdatu.security.account.UnverifiedAccountException;
  49. import org.amdatu.security.password.hash.PasswordHasher;
  50. import org.amdatu.security.tokenprovider.TokenProvider;
  51. import org.amdatu.security.tokenprovider.TokenProviderException;
  52. import org.osgi.service.cm.ConfigurationException;
  53. import org.osgi.service.cm.ManagedService;
  54. import org.osgi.service.event.Event;
  55. import org.osgi.service.event.EventAdmin;
  56. import org.osgi.service.log.LogService;
  57. /**
  58. * Default implementation of {@link AccountAdmin}.
  59. */
  60. public class AccountAdminImpl implements AccountAdmin, ManagedService {
  61. // Injected by Felix DM...
  62. private volatile AccountAdminBackend m_backend;
  63. private volatile AccountValidator m_accountValidator;
  64. private volatile PasswordHasher m_passwordHasher;
  65. private volatile TokenProvider m_tokenProvider;
  66. private volatile EventAdmin m_eventAdmin;
  67. private volatile LogService m_log;
  68. // Locally managed...
  69. private volatile AccountAdminConfig m_config;
  70. /**
  71. * Creates a new {@link AccountAdminImpl} instance.
  72. */
  73. public AccountAdminImpl() {
  74. m_config = new AccountAdminConfig();
  75. }
  76. /**
  77. * Creates a new {@link AccountAdminImpl} instance.
  78. */
  79. protected AccountAdminImpl(AccountAdminBackend backend, PasswordHasher passwordHasher) {
  80. m_backend = Objects.requireNonNull(backend);
  81. m_passwordHasher = Objects.requireNonNull(passwordHasher);
  82. m_config = new AccountAdminConfig();
  83. }
  84. @Override
  85. public boolean accountExists(String accountId) {
  86. if (accountId == null) {
  87. return false;
  88. }
  89. return m_backend.accountExists(accountId);
  90. }
  91. @Override
  92. public Account createAccount(Map<String, String> credentials) throws AccountExistsException {
  93. AccountAdminConfig cfg = m_config;
  94. verifyCredentialsValid(cfg, credentials);
  95. String accountId = credentials.get(cfg.getAccountIdKey());
  96. if (m_backend.accountExists(accountId)) {
  97. log(LOG_WARNING, "Invalid account creation: account already exist!");
  98. throw new AccountExistsException();
  99. }
  100. String token;
  101. try {
  102. token = generateAccessToken(cfg, accountId);
  103. }
  104. catch (IllegalArgumentException | TokenProviderException e) {
  105. log(LOG_WARNING, "Failed to generate access token for account: %s", e, accountId);
  106. throw new AccountException("token_generation_failed");
  107. }
  108. State state = State.NORMAL;
  109. if (cfg.isAccountVerificationNeeded()) {
  110. state = State.ACCOUNT_VERIFICATION_NEEDED;
  111. }
  112. Account account =
  113. new Account(accountId, hashSensitiveCredentials(cfg, credentials), state, token, false /* locked */);
  114. validateAccount(account, credentials);
  115. // Will throw an exception in case the creation fails...
  116. m_backend.create(account);
  117. // Notify others about this...
  118. postEvent(TOPIC_ACCOUNT_CREATED, KEY_ACCOUNT_ID, accountId, KEY_ACCOUNT_STATE, account.getState(),
  119. KEY_ACCOUNT_ACCESS_TOKEN, token, KEY_ACCOUNT_CREDENTIALS, account.getCredentials());
  120. return account;
  121. }
  122. @Override
  123. public Optional<Account> getAccount(Map<String, String> credentials)
  124. throws AccountLockedException, AccountCredentialResetException {
  125. AccountAdminConfig cfg = m_config;
  126. // Work on a mutable copy of the given credentials...
  127. Map<String, String> creds = new HashMap<>(credentials);
  128. String accountId = creds.get(cfg.getAccountIdKey());
  129. if (accountId == null) {
  130. log(LOG_INFO, "Unable to retrieve account: credentials are missing property '%s'!", cfg.getAccountIdKey());
  131. return Optional.empty();
  132. }
  133. return m_backend.get(accountId)
  134. .map(account -> {
  135. if (account.isLocked()) {
  136. throw new AccountLockedException();
  137. }
  138. else if (account.isForcedCredentialReset()) {
  139. throw new AccountCredentialResetException("reset_needed");
  140. }
  141. else if (account.isAccountVerificationNeeded()) {
  142. throw new UnverifiedAccountException();
  143. }
  144. boolean verificationResult = verifyCredentials(cfg, account.getCredentials(), creds);
  145. if (verificationResult && account.isVoluntaryCredentialReset()) {
  146. // Account logged in successfully with existing credentials, which implies that a reset token wasn't necessary after all...
  147. account.clearAccessToken();
  148. // Store the changes...
  149. m_backend.update(account);
  150. }
  151. else if (!verificationResult) {
  152. // Nope, caller didn't prove he was the one...
  153. account = null;
  154. }
  155. return account;
  156. });
  157. }
  158. @Override
  159. public Account getAccountByAccessToken(String accessToken) throws AccountLockedException, NoSuchAccountException {
  160. AccountAdminConfig cfg = m_config;
  161. String accountId;
  162. try {
  163. accountId = getAccountIdFromAccessToken(accessToken);
  164. }
  165. catch (IllegalArgumentException | TokenProviderException e) {
  166. log(LOG_INFO, "Attempting to get accountID from token failed!", e);
  167. throw new NoSuchAccountException();
  168. }
  169. Account account = getExistingAccount(accountId);
  170. verifyAccessToken(cfg, account, accessToken)
  171. .ifPresent(reason -> {
  172. log(LOG_INFO, "Attempting to get account for %s failed: %s!", account.getId(), reason);
  173. throw new NoSuchAccountException();
  174. });
  175. return account;
  176. }
  177. @Override
  178. public Account getAccountById(String accountId) throws AccountLockedException, NoSuchAccountException {
  179. return getExistingAccount(accountId);
  180. }
  181. @Override
  182. public Account removeAccount(String accountId) throws NoSuchAccountException, AccountException {
  183. AccountAdminConfig cfg = m_config;
  184. // This is an optional operation, and might not be desired in all use cases...
  185. if (!cfg.isAccountRemovalAllowed()) {
  186. throw new AccountException("not_allowed");
  187. }
  188. try {
  189. Account account = getExistingAccount(accountId);
  190. // Store the changes...
  191. m_backend.remove(account);
  192. // Notify other of this (for example, send an email or something)...
  193. postEvent(TOPIC_ACCOUNT_REMOVED, KEY_ACCOUNT_ID, accountId, KEY_ACCOUNT_STATE, account.getState(),
  194. KEY_ACCOUNT_CREDENTIALS, filterSensitiveCredentials(cfg, account.getCredentials()));
  195. return account;
  196. }
  197. catch (UnsupportedOperationException e) {
  198. log(LOG_WARNING, "Cannot remove account '%s': operation not supported by backend!", accountId);
  199. throw new AccountException("not_allowed");
  200. }
  201. }
  202. @Override
  203. public Account resetCredentials(String accountId, boolean forced) {
  204. AccountAdminConfig cfg = m_config;
  205. try {
  206. Account account = getExistingAccount(accountId);
  207. // Generate the reset-credential token...
  208. String token = generateAccessToken(cfg, accountId);
  209. // Update the account...
  210. account.resetCredentials(token, forced);
  211. // Store the changes...
  212. m_backend.update(account);
  213. // Notify other of this (for example, send an email or something)...
  214. postEvent(TOPIC_ACCOUNT_RESET_CREDENTIALS, KEY_ACCOUNT_ID, accountId, KEY_ACCOUNT_STATE, account.getState(),
  215. KEY_ACCOUNT_ACCESS_TOKEN, token, KEY_ACCOUNT_CREDENTIALS,
  216. filterSensitiveCredentials(cfg, account.getCredentials()));
  217. return account;
  218. }
  219. catch (IllegalStateException e) {
  220. log(LOG_WARNING, "Cannot reset credentials for unverified account: %s", e, accountId);
  221. throw new AccountCredentialResetException("unverified_account");
  222. }
  223. catch (IllegalArgumentException | TokenProviderException e) {
  224. log(LOG_WARNING, "Failed to generate reset credentials token for account: %s", e, accountId);
  225. throw new AccountException("token_generation_failed");
  226. }
  227. }
  228. @Override
  229. public Account updateAccount(Map<String, String> oldCredentials, Map<String, String> newCredentials)
  230. throws NoSuchAccountException {
  231. AccountAdminConfig cfg = m_config;
  232. Optional<Account> account = getAccount(oldCredentials);
  233. if (!account.isPresent()) {
  234. throw new NoSuchAccountException();
  235. }
  236. return updateCredentials(cfg, account.get(), newCredentials);
  237. }
  238. @Override
  239. public Account updateAccount(Map<String, String> credentials, String resetToken)
  240. throws NoSuchAccountException, AccountLockedException {
  241. AccountAdminConfig cfg = m_config;
  242. Account account = getAccountByAccessToken(resetToken);
  243. // No need to verify the reset token twice: we can directly update the credentials for this one...
  244. return updateCredentials(cfg, account, credentials);
  245. }
  246. @Override
  247. public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
  248. AccountAdminConfig newConfig;
  249. if (properties != null) {
  250. newConfig = new AccountAdminConfig(properties);
  251. }
  252. else {
  253. newConfig = new AccountAdminConfig();
  254. }
  255. m_config = newConfig;
  256. }
  257. @Override
  258. public Account verifyAccount(String accountId, String verificationToken) throws NoSuchAccountException {
  259. AccountAdminConfig cfg = m_config;
  260. Account account = getExistingAccount(accountId);
  261. verifyAccessToken(cfg, account, verificationToken)
  262. .ifPresent(reason -> {
  263. log(LOG_INFO, "Attempting to verify account for %s failed: %s!", account.getId(), reason);
  264. throw new AccountException(reason);
  265. });
  266. return updateCredentials(cfg, account, account.getCredentials());
  267. }
  268. private Event createEvent(String topic, Object... eventProps) {
  269. Map<String, Object> props = new HashMap<>();
  270. for (int i = 0; i < eventProps.length; i += 2) {
  271. props.put(eventProps[i].toString(), eventProps[i + 1]);
  272. }
  273. return new Event(topic, props);
  274. }
  275. private Map<String, String> filterSensitiveCredentials(AccountAdminConfig cfg, Map<String, String> credentials) {
  276. Set<String> sensitiveCredentials = cfg.getSensitiveCredentials();
  277. return credentials.entrySet().stream()
  278. .filter(e -> !sensitiveCredentials.contains(e.getKey()))
  279. .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
  280. }
  281. private String generateAccessToken(AccountAdminConfig cfg, String accountId) {
  282. Instant notBefore = Instant.now();
  283. Instant notAfter = notBefore.plus(cfg.getResetTokenLifetime(), ChronoUnit.HOURS);
  284. Map<String, String> attrs = new HashMap<>();
  285. attrs.put(SUBJECT, accountId);
  286. attrs.put(NOT_BEFORE, Long.toString(notBefore.getEpochSecond()));
  287. attrs.put(EXPIRATION_TIME, Long.toString(notAfter.getEpochSecond()));
  288. return m_tokenProvider.generateToken(attrs);
  289. }
  290. private String getAccountIdFromAccessToken(String accessToken)
  291. throws IllegalArgumentException, TokenProviderException {
  292. Map<String, String> attrs = m_tokenProvider.verifyToken(accessToken);
  293. return attrs.get(SUBJECT);
  294. }
  295. private Account getExistingAccount(String accountId) {
  296. return m_backend.get(accountId)
  297. .map(account -> {
  298. if (account.isLocked()) {
  299. log(LOG_INFO, "Obtaining account for %s failed: account is locked!", accountId);
  300. throw new AccountLockedException();
  301. }
  302. return account;
  303. }).orElseThrow(() -> {
  304. log(LOG_INFO, "Obtaining account for %s failed: no such account!", accountId);
  305. return new NoSuchAccountException();
  306. });
  307. }
  308. private Map<String, String> hashSensitiveCredentials(AccountAdminConfig cfg, Map<String, String> credentials) {
  309. Set<String> sensitiveCredentials = cfg.getSensitiveCredentials();
  310. return credentials.entrySet().stream()
  311. .collect(toMap(Map.Entry::getKey, e -> {
  312. String value = e.getValue();
  313. // By default we're going to hash anything that looks like a password...
  314. if (sensitiveCredentials.contains(e.getKey()) && !m_passwordHasher.isHashed(value)) {
  315. value = m_passwordHasher.hash(value);
  316. }
  317. return value;
  318. }));
  319. }
  320. private void log(int level, String msg, Object... args) {
  321. log(level, msg, null, args);
  322. }
  323. private void log(int level, String msg, Throwable e, Object... args) {
  324. m_log.log(level, String.format(msg, args), e);
  325. }
  326. private void postEvent(String topic, Object... eventProps) {
  327. m_eventAdmin.postEvent(createEvent(topic, eventProps));
  328. }
  329. private Account updateCredentials(AccountAdminConfig cfg, Account account, Map<String, String> credentials) {
  330. log(LOG_INFO, "Updating credentials for account: %s", account.getId());
  331. account.clearAccessToken();
  332. account.setCredentials(hashSensitiveCredentials(cfg, credentials));
  333. // Will throw an exception in case validation fails...
  334. validateAccount(account, credentials);
  335. // Will throw an exception in case it fails to update the account...
  336. m_backend.update(account);
  337. log(LOG_DEBUG, "Credentials updated for account: %s", account.getId());
  338. // Notify others of the changes in this account...
  339. postEvent(TOPIC_ACCOUNT_UPDATED, KEY_ACCOUNT_ID, account.getId(), KEY_ACCOUNT_STATE, account.getState(),
  340. KEY_ACCOUNT_CREDENTIALS, filterSensitiveCredentials(cfg, account.getCredentials()));
  341. return account;
  342. }
  343. /**
  344. * @param account the account to validate, cannot be <code>null</code>;
  345. * @param credentials the <b>unhashed</b> credentials of the account.
  346. */
  347. private void validateAccount(Account account, Map<String, String> credentials) {
  348. // work on a copy to avoid the original being modified (maliciously or accidentally)...
  349. Account copy = new Account(account);
  350. copy.setCredentials(credentials);
  351. m_accountValidator.validate(copy);
  352. }
  353. private Optional<String> verifyAccessToken(AccountAdminConfig cfg, Account account, String accessToken) {
  354. Optional<String> accountResetToken = account.getAccessToken();
  355. if (!accountResetToken.isPresent()) {
  356. return Optional.of("no_reset_token");
  357. }
  358. if (!accountResetToken.get().equals(accessToken)) {
  359. return Optional.of("invalid_reset_token");
  360. }
  361. Map<String, String> attrs;
  362. try {
  363. attrs = m_tokenProvider.verifyToken(accessToken);
  364. }
  365. catch (IllegalArgumentException | TokenProviderException e) {
  366. return Optional.of("invalid_reset_token");
  367. }
  368. if (!account.getId().equals(attrs.get(SUBJECT))) {
  369. return Optional.of("invalid_reset_token");
  370. }
  371. // All preconditions are verified, we can "safely" update the account...
  372. return Optional.empty();
  373. }
  374. private boolean verifyCredentials(AccountAdminConfig cfg, Map<String, String> expected,
  375. Map<String, String> actual) {
  376. Set<String> checkedCreds = cfg.getCheckedCredentials();
  377. boolean verificationResult = true;
  378. for (String key : checkedCreds) {
  379. String expectedValue = expected.getOrDefault(key, "");
  380. String actualValue = actual.getOrDefault(key, "");
  381. // Do *not* break out of the loop in case the verification fails, this
  382. // would make this routine susceptible to a timing attack. For instance,
  383. // if the email credential is checked *prior* to the password credential,
  384. // the fact that this method returns early might reveal this to a
  385. // malicious user which can guess which accounts are available and which
  386. // are not...
  387. verificationResult &= m_passwordHasher.verify(expectedValue, actualValue);
  388. }
  389. return verificationResult;
  390. }
  391. private void verifyCredentialsValid(AccountAdminConfig cfg, Map<String, String> credentials) {
  392. Set<String> checkedCreds = cfg.getCheckedCredentials();
  393. boolean credentialValid = checkedCreds.stream()
  394. .allMatch(k -> credentials.containsKey(k) && credentials.get(k) != null);
  395. if (!credentialValid) {
  396. log(LOG_WARNING, "Invalid account creation: not all required credentials present: " + checkedCreds + "!");
  397. throw new AccountException();
  398. }
  399. }
  400. }