Java tutorial
/******************************************************************************* * Copyright Squid Solutions, 2016 * * This file is part of Open Bouquet software. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation (version 3 of the License). * * There is a special FOSS exception to the terms and conditions of the * licenses as they are applied to this program. See LICENSE.txt in * the directory of this program distribution. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * * Squid Solutions also offers commercial licenses with additional warranties, * professional functionalities or services. If you purchase a commercial * license, then it supersedes and replaces any other agreement between * you and Squid Solutions (above licenses and LICENSE.txt included). * See http://www.squidsolutions.com/EnterpriseBouquet/ *******************************************************************************/ package com.squid.kraken.v4.api.core.customer; import java.net.MalformedURLException; import java.net.URL; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import javax.mail.MessagingException; import org.apache.commons.codec.binary.Base64; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.MalformedClaimException; import org.jose4j.jwt.consumer.InvalidJwtException; import org.jose4j.jwt.consumer.JwtConsumer; import org.jose4j.jwt.consumer.JwtConsumerBuilder; import org.jose4j.jwt.consumer.JwtContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Optional; import com.squid.kraken.v4.KrakenConfig; import com.squid.kraken.v4.api.core.APIException; import com.squid.kraken.v4.api.core.EmailHelper; import com.squid.kraken.v4.api.core.GenericServiceImpl; import com.squid.kraken.v4.api.core.InvalidCredentialsAPIException; import com.squid.kraken.v4.api.core.ModelGC; import com.squid.kraken.v4.api.core.ObjectNotFoundAPIException; import com.squid.kraken.v4.api.core.ServiceUtils; import com.squid.kraken.v4.model.AccessToken; import com.squid.kraken.v4.model.AccessToken.Type; import com.squid.kraken.v4.model.AccessTokenPK; import com.squid.kraken.v4.model.AuthCode; import com.squid.kraken.v4.model.Client; import com.squid.kraken.v4.model.ClientPK; import com.squid.kraken.v4.model.Customer; import com.squid.kraken.v4.model.CustomerInfo; import com.squid.kraken.v4.model.CustomerPK; import com.squid.kraken.v4.model.User; import com.squid.kraken.v4.persistence.AppContext; import com.squid.kraken.v4.persistence.DAOFactory; import com.squid.kraken.v4.persistence.DataStoreQueryField; import com.squid.kraken.v4.persistence.dao.AccessTokenDAO; import com.squid.kraken.v4.persistence.dao.ClientDAO; import com.squid.kraken.v4.persistence.dao.UserDAO; public class AuthServiceImpl extends GenericServiceImpl<AccessToken, AccessTokenPK> { final Logger logger = LoggerFactory.getLogger(AuthServiceImpl.class); private ScheduledExecutorService modelGC; private ScheduledFuture<?> modelGCThread; private static AuthServiceImpl instance; public static AuthServiceImpl getInstance() { if (instance == null) { instance = new AuthServiceImpl(); } return instance; } protected DAOFactory factory = DAOFactory.getDAOFactory(); private AuthServiceImpl() { // made private for singleton access super(AccessToken.class); } public void initGC() { modelGC = Executors.newSingleThreadScheduledExecutor(); ModelGC<AccessToken, AccessTokenPK> gc = new ModelGC<AccessToken, AccessTokenPK>(0, this, AccessToken.class); modelGCThread = modelGC.scheduleWithFixedDelay(gc, 0, 1, TimeUnit.HOURS); } public void stopGC() { try { logger.info("stopping GC scheduler for " + this.getClass().getName()); if (modelGCThread != null) { modelGCThread.cancel(true); } if (modelGC != null) { modelGC.shutdown(); modelGC.awaitTermination(2, TimeUnit.SECONDS); modelGC.shutdownNow(); } } catch (Exception e) { e.printStackTrace(); } } /** * Authenticate a {@link User} and return a new {@link AccessToken}.<br> * Note : Tokens received on the fragment MUST be explicitly validated by * calling /tokeninfo endpoint.<br> * Failure to verify tokens acquired this way makes your application more * vulnerable to the confused deputy problem. * * @param clientId * @param redirectUrl * @param login * user login * @param password * user password * @return an {@link AccessToken} set with default validity */ public AccessToken authAndReturnToken(AppContext ctx, ClientPK clientId, String redirectUrl, String login, String password) throws DuplicateUserException { User user = auth(ctx, clientId, redirectUrl, login, password); return ServiceUtils.getInstance().createToken(user.getCustomerId(), clientId, user.getId().getUserId(), System.currentTimeMillis(), ServiceUtils.getInstance().getTokenExpirationPeriodMillis(), null, null); } /** * Authenticate a {@link User}.<br> * * @param ctx * @param clientId * @param redirectUrl * @param login * user login * @param password * user password * @return a User */ public User auth(AppContext ctx, ClientPK clientId, String redirectUrl, String login, String password) throws DuplicateUserException { if (clientId == null) { throw new ObjectNotFoundAPIException("ClientId must be provided", ctx.isNoError()); } CustomerServiceBaseImpl customerService = CustomerServiceBaseImpl.getInstance(); String customerId = ctx.getCustomerId(); if ((customerId == null) || customerId.isEmpty()) { // Build a custom query to search for users across all customers AppContext emptyContext = new AppContext.Builder().build(); List<DataStoreQueryField> queryFields = new LinkedList<DataStoreQueryField>(); queryFields.add(new DataStoreQueryField("login", login.toLowerCase())); List<User> users = DAOFactory.getDAOFactory().getBaseDataStore().find(emptyContext, User.class, queryFields, null); List<User> usersMatched = new ArrayList<User>(); if (users.size() == 0) { logger.info("login failed (user not found) for user-login : '" + login + "'"); throw new ObjectNotFoundAPIException("User not found for given login / password (ERR1)", ctx.isNoError()); } // filter by password matching String loginExceptionMessage = null; for (User user : users) { // Get the Customer using root context. CustomerPK cId = new CustomerPK(user.getCustomerId()); AppContext root = ServiceUtils.getInstance().getRootUserContext(cId.getCustomerId()); Customer customerPrivate = customerService.read(root, cId); if (ServiceUtils.getInstance().matchPassword(customerPrivate, user, password)) { // check the client before adding the the matched users ClientPK clientIdtmp = new ClientPK(user.getCustomerId(), clientId.getClientId()); try { verifyClient(clientIdtmp, redirectUrl, ctx.isNoError()); usersMatched.add(user); } catch (RuntimeException e) { loginExceptionMessage = e.getMessage(); logger.info( "Client check failed for user " + user.getLogin() + " : " + loginExceptionMessage); } } else { loginExceptionMessage = "Password check failed"; } } if (usersMatched.size() == 0) { if (users.size() <= 1) { loginExceptionMessage = "User not found for given login / password (ERR2)"; } logger.info("Invalid login for user " + login + " : " + loginExceptionMessage); throw new ObjectNotFoundAPIException(loginExceptionMessage, ctx.isNoError()); } else if (usersMatched.size() > 1) { // multiple users found with same login/pwd List<CustomerInfo> customers = new ArrayList<CustomerInfo>(); for (User user : usersMatched) { customers.add(customerService.readCustomerInfo(user.getCustomerId())); } throw new DuplicateUserException(ctx.isNoError(), customers); } else { // unique user found customerId = usersMatched.get(0).getCustomerId(); } } User user = checkLogin(ctx, customerId, clientId.getClientId(), redirectUrl, login, password); return user; } /** * Authenticate a {@link User} and return a new {@link AuthCode}.<br> * * @param userContext * .getCustomerId() * @param clientId * @param redirectUrl * @param login * user login * @param password * user password * @return an {@link AuthCode} */ public AuthCode authAndReturnCode(AppContext anonymousCtx, ClientPK clientId, String redirectUrl, String login, String password, boolean generateRefreshToken) { User user = auth(anonymousCtx, clientId, redirectUrl, login, password); String refreshTokenId; if (generateRefreshToken) { AccessTokenDAO dao = (AccessTokenDAO) DAOFactory.getDAOFactory().getDAO(AccessToken.class); // create a refresh token if not already existing Optional<AccessToken> findRefreshToken = dao.findRefreshToken(user.getCustomerId(), clientId.getClientId(), user.getOid()); AccessToken refreshToken; if (findRefreshToken.isPresent()) { refreshToken = findRefreshToken.get(); } else { refreshToken = createRefreshToken(user.getCustomerId(), clientId, user.getId().getUserId()); } // store its id in the authCode refreshTokenId = refreshToken.getId().getTokenId(); } else { refreshTokenId = null; } AccessToken authCode = createAuthCode(user.getCustomerId(), clientId, user.getId().getUserId(), refreshTokenId); return new AuthCode(authCode.getId().getTokenId()); } /** * Exchange an authorization code with an access token.<br> * * @param ctx * @param clientId * @param redirectUrl * @param authorizationCode */ public AccessToken getTokenFromAuthCode(AppContext ctx, ClientPK clientId, String redirectUrl, String authorizationCode) { AccessToken codeToken; try { codeToken = ServiceUtils.getInstance().getToken(authorizationCode); } catch (TokenExpiredException e) { throw new InvalidCredentialsAPIException("Access Code has expired", ctx.isNoError()); } // check code token // TODO check redirect URL if (codeToken == null) { throw new InvalidCredentialsAPIException("Invalid Access Code", ctx.isNoError()); } if (!codeToken.getClientId().equals(clientId.getClientId())) { throw new InvalidCredentialsAPIException("Invalid Access Code (ERR0)", ctx.isNoError()); } // create a new access token AccessToken token = ServiceUtils.getInstance().createToken(codeToken.getCustomerId(), clientId, codeToken.getUserId(), System.currentTimeMillis(), ServiceUtils.getInstance().getTokenExpirationPeriodMillis(), null, null); // set refresh token if (codeToken.getRefreshToken() != null) { token.setRefreshToken(codeToken.getRefreshToken()); } // delete the codeToken DAOFactory.getDAOFactory().getDAO(AccessToken.class).delete(ctx, codeToken.getId()); return token; } public AccessToken getTokenFromRefreshToken(AppContext ctx, ClientPK clientId, String clientSecret, String redirectUrl, String refreshTokenValue) { AccessTokenDAO dao = (AccessTokenDAO) DAOFactory.getDAOFactory().getDAO(AccessToken.class); AccessToken refreshToken; Optional<AccessToken> findRefreshToken = dao.findRefreshToken(refreshTokenValue); // check token // TODO check redirect URL & Client Secret if (!findRefreshToken.isPresent()) { throw new InvalidCredentialsAPIException("Invalid refresh token (ERR0)", ctx.isNoError()); } else { refreshToken = findRefreshToken.get(); } if (!refreshToken.getClientId().equals(clientId.getClientId())) { throw new InvalidCredentialsAPIException("Invalid refresh token (ERR1)", ctx.isNoError()); } // create a new access token AccessToken token = ServiceUtils.getInstance().createToken(refreshToken.getCustomerId(), clientId, refreshToken.getUserId(), System.currentTimeMillis(), ServiceUtils.getInstance().getTokenExpirationPeriodMillis(), null, null); token.setRefreshToken(refreshToken.getOid()); return token; } public AccessToken getTokenFromJWT(AppContext ctx, String jwt) { try { // first pass to read the issuer (client Id) JwtConsumer firstPassJwtConsumer = new JwtConsumerBuilder().setSkipAllValidators() .setDisableRequireSignature().setSkipSignatureVerification().build(); JwtContext jwtContext = firstPassJwtConsumer.process(jwt); JwtClaims claims = jwtContext.getJwtClaims(); String issuer = claims.getIssuer(); String customerId = claims.getStringClaimValue("customerId"); ClientPK clientId = new ClientPK(claims.getStringClaimValue("customerId"), issuer); // load the client using superuser to get the key AppContext rootUserContext = ServiceUtils.getInstance().getRootUserContext(customerId); Client client = DAOFactory.getDAOFactory().getDAO(Client.class).readNotNull(rootUserContext, clientId); String publicKeyPEM = client.getJWTKeyPublic(); publicKeyPEM = publicKeyPEM.substring(publicKeyPEM.indexOf('\n'), publicKeyPEM.lastIndexOf('\n')); publicKeyPEM = publicKeyPEM.replace("\n", ""); byte[] publicKey = Base64.decodeBase64(publicKeyPEM); KeySpec keySpec = new X509EncodedKeySpec(publicKey); KeyFactory kf = KeyFactory.getInstance("RSA"); PublicKey key = kf.generatePublic(keySpec); JwtConsumer jwtConsumer = new JwtConsumerBuilder().setRequireExpirationTime() // the JWT must have an expiration time .setAllowedClockSkewInSeconds(30) // allow some leeway in validating time based claims to account for clock skew .setRequireSubject() // the JWT must have a subject claim .setVerificationKey(key) // verify the signature with the public key .build(); // create the JwtConsumer instance // validate the JWT jwtConsumer.processContext(jwtContext); // create the token String userId = jwtContext.getJwtClaims().getSubject(); AccessToken token = ServiceUtils.getInstance().createToken(customerId, clientId, userId, System.currentTimeMillis(), ServiceUtils.getInstance().getTokenExpirationPeriodMillis(), null, null); return token; } catch (MalformedClaimException e) { logger.debug(e.getMessage()); throw new InvalidCredentialsAPIException("Invalid JWT Claim", ctx.isNoError()); } catch (InvalidJwtException e) { logger.debug(e.getMessage()); throw new InvalidCredentialsAPIException("Invalid JWT", ctx.isNoError()); } catch (NoSuchAlgorithmException e) { logger.debug(e.getMessage()); throw new RuntimeException(e); } catch (InvalidKeySpecException e) { logger.debug(e.getMessage()); throw new RuntimeException(e); } } /** * Check {@link User} login.<br> * Redirect URL check will be performed to make sure its domain matches a * least one of the Client URLs (bypassed if no client URLS defined). * * @param userContext * .getCustomerId() * @param clientId * @param redirectUrl * @param login * user login * @param password * user password * @return a {@link User} */ private User checkLogin(AppContext ctx, String customerId, String clientId, String redirectUrl, String login, String password) { AppContext rootctx = ServiceUtils.getInstance().getRootUserContext(customerId); ClientPK clientPk = new ClientPK(customerId, clientId); try { verifyClient(clientPk, redirectUrl, rootctx.isNoError()); } catch (RuntimeException e) { logger.info(e.getMessage()); throw e; } // user login lookup UserDAO userDAO = (UserDAO) factory.getDAO(User.class); Optional<User> userOpt = userDAO.findByLogin(rootctx, login); if (userOpt.isPresent()) { User user = userOpt.get(); if (ServiceUtils.getInstance().matchPassword(rootctx, user, password)) { if (!user.isSuperUser()) { logger.info("login(" + customerId + "," + clientId + "," + user.getId().getUserId() + ")"); return user; } else { logger.info("login failed (super user) for user-login : '" + login + "' [customerId :" + customerId + "]"); throw new InvalidCredentialsAPIException("sorry, this user cannot log-in", ctx.isNoError()); } } else { logger.info("login failed (invalid pwd) for user-login : '" + login + "' [customerId :" + customerId + "]"); } } else { logger.info("login failed (user not found) for user-login : '" + login + "' [customerId :" + customerId + "]"); } throw new ObjectNotFoundAPIException("User not found for given login / password", ctx.isNoError()); } public void logoutUser(AppContext ctx) { logger.info(ctx.getUser() + " logoutUser"); AccessTokenDAO dao = ((AccessTokenDAO) DAOFactory.getDAOFactory().getDAO(AccessToken.class)); List<AccessToken> findByUser = dao.findByUser(ctx, ctx.getUser().getId()); for (AccessToken token : findByUser) { dao.delete(ctx, token.getId()); } } private void verifyClient(ClientPK clientId, String redirectUrl, boolean isNoError) { String serverMode = KrakenConfig.getProperty("kraken.server.mode", true); if ((serverMode != null) && (serverMode.equals("dev"))) { logger.info("DevMode : bypassing clientId check"); return; } if (clientId != null) { // client lookup ClientDAO clientDAO = (ClientDAO) factory.getDAO(Client.class); Client client; AppContext ctx = ServiceUtils.getInstance().getRootUserContext(clientId.getCustomerId()); Optional<Client> clientOpt = clientDAO.read(ctx, clientId); if (!clientOpt.isPresent()) { throw new ObjectNotFoundAPIException("Client Id not found", isNoError); } else { client = clientOpt.get(); } if (redirectUrl != null) { // redirect url validation try { boolean ok = false; if (client.getUrls().isEmpty()) { ok = true; } else { URL redirect = new URL(redirectUrl); for (String urlS : client.getUrls()) { try { if (urlS.equals(redirect.getHost())) { ok = true; } } catch (Exception e) { // should not happen logger.warn(e.getMessage(), e); } } } if (!ok) { throw new InvalidCredentialsAPIException("Redirect URL not allowed", isNoError); } } catch (MalformedURLException e) { throw new APIException("Redirect URL is malformed", isNoError); } } } else { throw new APIException("Client id must be provided", isNoError); } } public List<AccessToken> resetUserPassword(AppContext anonymousCtx, EmailHelper emailHelper, String clientId, String email, String lang, String url, String emailContent, String emailSubject) { // findUserByEmail if (email == null) { throw new ObjectNotFoundAPIException("User not found for given email", anonymousCtx.isNoError()); } List<User> users; String customerId = anonymousCtx.getCustomerId(); AppContext emptyContext = new AppContext.Builder().build(); if ((customerId == null) || customerId.isEmpty()) { // Build a custom query to search for users across all customers List<DataStoreQueryField> queryFields = new LinkedList<DataStoreQueryField>(); queryFields.add(new DataStoreQueryField("email", email.toLowerCase())); users = DAOFactory.getDAOFactory().getBaseDataStore().find(emptyContext, User.class, queryFields, null); if (users.size() == 0) { logger.info("user not found for user-email : '" + email + "'"); throw new ObjectNotFoundAPIException("User not found for given email", anonymousCtx.isNoError()); } } else { AppContext rootctx = ServiceUtils.getInstance().getRootUserContext(customerId); UserDAO userDAO = (UserDAO) factory.getDAO(User.class); Optional<User> userOpt = userDAO.findByEmail(rootctx, email); if (userOpt.isPresent()) { users = new ArrayList<User>(); users.add(userOpt.get()); } else { logger.info("user not found for user-email : '" + email + "' and customer : " + customerId); throw new ObjectNotFoundAPIException("User not found for given email", anonymousCtx.isNoError()); } } String resetLink = ""; List<AccessToken> tokens = new ArrayList<AccessToken>(); for (User user : users) { String custId = user.getCustomerId(); AppContext rootctx = ServiceUtils.getInstance().getRootUserContext(custId); CustomerInfo customerInfo = CustomerServiceBaseImpl.getInstance().readCustomerInfo(rootctx); ClientPK clientPk = new ClientPK(custId, clientId); try { verifyClient(clientPk, url, rootctx.isNoError()); } catch (RuntimeException e) { logger.info(e.getMessage()); clientPk = null; } // createToken if (clientPk != null) { AccessToken token = ServiceUtils.getInstance().createToken(custId, clientPk, user.getId().getUserId(), System.currentTimeMillis(), ServiceUtils.getInstance().getResetPasswordTokenExpirationPeriodMillis(), AccessToken.Type.RESET_PWD, null); tokens.add(token); // processResetPasswordTemplate String link = url.replace('{' + ServiceUtils.TOKEN_PARAM + "}", token.getId().getTokenId()); try { new URL(link); } catch (MalformedURLException e1) { throw new APIException("Invalid link url", e1, anonymousCtx.isNoError()); } if (users.size() > 1) { String customerName = customerInfo.getName(); if (customerName == null) { customerName = "id:" + customerInfo.getId(); } resetLink += "Customer '" + customerName + "' : " + link + "\n"; } else { resetLink = link; } } } if (tokens.size() == 0) { logger.info("No valid user found for user-email : '" + email + "' and customer : " + customerId); throw new ObjectNotFoundAPIException("User not found for given email", anonymousCtx.isNoError()); } else { emailContent = emailContent.replace("${validity}", "" + (ServiceUtils.getInstance().getTokenExpirationPeriodMillis() / 3600000)); emailContent = emailContent.replace("${resetLink}", resetLink); // sendMail List<String> dests = Arrays.asList(email); try { logger.info("Sending password reset token to " + email); emailHelper.sendEmail(dests, emailSubject, emailContent, null, EmailHelper.PRIORITY_NORMAL); return tokens; } catch (MessagingException e) { throw new APIException(e, anonymousCtx.isNoError()); } } } private AccessToken createRefreshToken(String customerId, ClientPK clientPk, String userId) { return ServiceUtils.getInstance().createToken(customerId, clientPk, userId, System.currentTimeMillis(), null, Type.REFRESH, null); } private AccessToken createAuthCode(String customerId, ClientPK clientPk, String userId, String refreshTokenId) { return ServiceUtils.getInstance().createToken(customerId, clientPk, userId, System.currentTimeMillis(), ServiceUtils.getInstance().getTokenExpirationPeriodMillis(), Type.CODE, refreshTokenId); } }