Oauth2Controller.java

package com.nonononoki.alovoa.rest;

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Map;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import com.nonononoki.alovoa.html.IndexResource;
import com.nonononoki.alovoa.html.LoginResource;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

import com.nonononoki.alovoa.Tools;
import com.nonononoki.alovoa.config.SecurityConfig;
import com.nonononoki.alovoa.entity.User;
import com.nonononoki.alovoa.model.AlovoaException;
import com.nonononoki.alovoa.repo.UserRepository;
import com.nonononoki.alovoa.service.PublicService;

@RestController
@RequestMapping("/")
public class Oauth2Controller {

	@Autowired
	private UserRepository userRepo;

	@Autowired
	private OAuth2AuthorizedClientService clientService;

	@Autowired
	private IndexResource indexResource;

	@Autowired
	private PublicService publicService;

	@Autowired
	private HttpSession httpSession;

	@Autowired
	private SecurityConfig securityConfig;

	@Autowired
	private HttpServletRequest request;

	@Autowired
	private HttpServletResponse response;

	@Value("${app.first-name.length-max}")
	private int firstNameMaxLength;

	private static final String REDIRECT_URL = "redirect-url";

	private static final Logger logger = LoggerFactory.getLogger(Oauth2Controller.class);

	private static final int REDIRECT_REGISTER = 1;
	private static final int REDIRECT_ONBOARDING = 2;
	private static final int REDIRECT_DEFAULT = 3;
	private static final int HOUR_S = 3600;

	@GetMapping("/oauth2/authorization/google/{redirectUrlEncoded}")
	public ModelAndView oauth2Google(@PathVariable String redirectUrlEncoded) {
		httpSession.setAttribute(REDIRECT_URL, redirectUrlEncoded);
		return new ModelAndView(new RedirectView("/oauth2/authorization/google"));
	}

	@GetMapping("/oauth2/authorization/facebook/{redirectUrlEncoded}")
	public ModelAndView oauth2Facebook(@PathVariable String redirectUrlEncoded) {
		httpSession.setAttribute(REDIRECT_URL, redirectUrlEncoded);
		return new ModelAndView(new RedirectView("/oauth2/authorization/facebook"));
	}

	@SuppressWarnings("rawtypes")
	@GetMapping("/login/oauth2/success")
	public ModelAndView oauth2Success() {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

		try {
			OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;

			String clientRegistrationId = oauthToken.getAuthorizedClientRegistrationId();

			OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(clientRegistrationId,
					oauthToken.getName());
			String endpoint = client.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri();

			if (!endpoint.isEmpty()) {
				RestTemplate template = new RestTemplate();
				HttpHeaders headers = new HttpHeaders();
				headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + client.getAccessToken().getTokenValue());
				HttpEntity<String> entity = new HttpEntity<>("", headers);

				// get user data via URL from the oauth2 provider
				ResponseEntity<Map> response = template.exchange(endpoint, HttpMethod.GET, entity, Map.class);
				Map attributes = response.getBody();

				if (attributes == null) {
					SecurityContextHolder.clearContext();
					throw new AlovoaException("oauth_attributes_not_found");
				}

				String firstName = (String) attributes.get("given_name"); // Google
				if (firstName == null) {
					firstName = (String) attributes.get("name"); // Facebook
					if (firstName.contains(" ")) {
						firstName = firstName.split(" ")[0];
						if (firstName.length() > firstNameMaxLength) {
							firstName = firstName.substring(0, firstNameMaxLength);
						}
					}
				}

				if (attributes.get("email") == null) {
					SecurityContextHolder.clearContext();
					throw new AlovoaException(publicService.text("backend.error.register.oauth.email-invalid"));
				}

				String email = (String) attributes.get("email");
				email = email.toLowerCase();

				User user = userRepo.findByEmail(email);
				if (user == null) {
					user = new User(email);
				}

				// administrator cannot use oauth for security reason e.g. password breach on
				// oath provider
				if (user.isAdmin()) {
					SecurityContextHolder.clearContext();
					throw new AlovoaException("not_supported_for_admin");
				}

				if (!user.isConfirmed()) {
					if (httpSession.getAttribute(REDIRECT_URL) != null) {
						String params = getOauthParams(email, firstName, REDIRECT_REGISTER);
						String url = new String(
								Base64.getDecoder().decode((String) httpSession.getAttribute(REDIRECT_URL)),
								StandardCharsets.UTF_8);
						httpSession.removeAttribute(REDIRECT_URL);
						return new ModelAndView(new RedirectView(url + params));
					}
					return indexResource.index();
				} else {
					if (httpSession.getAttribute(REDIRECT_URL) != null) {
						int page = REDIRECT_DEFAULT;
						if (!user.isAdmin() && user.getProfilePicture() == null && user.getDescription() == null) {
							page = REDIRECT_ONBOARDING;
						}
						String params = getOauthParams(email, firstName, page);
						String url = new String(
								Base64.getDecoder().decode((String) httpSession.getAttribute(REDIRECT_URL)),
								StandardCharsets.UTF_8);
						httpSession.removeAttribute(REDIRECT_URL);
						return new ModelAndView(new RedirectView(url + params));
					}
					return new ModelAndView("redirect:" + LoginResource.URL);
				}
			}
		} catch (AlovoaException e) {
			return new ModelAndView("redirect:/?register.oauth.email-invalid");
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		}

		return new ModelAndView("redirect:" + LoginResource.URL);
	}

	@GetMapping("/oauth2/remember-me-cookie/{rememberCookieValue}/{sessionId}")
	public void getRememberMeCookie(@PathVariable String rememberCookieValue, @PathVariable String sessionId) {
		Cookie cookieRememberMe = securityConfig.getOAuthRememberMeServices().getRememberMeCookie(rememberCookieValue, request, response);
		response.addCookie(cookieRememberMe);
		
		Cookie cookieSession = new Cookie(SecurityConfig.COOKIE_SESSION, sessionId);
		cookieSession.setMaxAge(HOUR_S);
		cookieSession.setPath("/");
		cookieSession.setSecure(request.isSecure());
		cookieSession.setHttpOnly(true);
		response.addCookie(cookieSession);
	}

	private String getOauthParams(String username, String firstName, int page) {
		return Tools.getAuthParams(securityConfig, httpSession.getId(), username, firstName, page);
	}
}