package io.kaif.web; import java.io.UnsupportedEncodingException; import java.util.Optional; import java.util.Set; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; import org.springframework.web.util.UriUtils; import com.google.common.base.Charsets; import com.google.common.base.Strings; import io.kaif.model.clientapp.ClientApp; import io.kaif.model.clientapp.ClientAppScope; import io.kaif.oauth.GrantType; import io.kaif.oauth.OauthErrorDto; import io.kaif.oauth.OauthErrors; import io.kaif.service.ClientAppService; import io.kaif.web.support.AccessDeniedException; /** * we have special settings in nginx for url prefix `/oauth`. so if * you want to change mapping url, please review nginx settins in ansible */ @Controller @RequestMapping("/oauth") public class OauthController { private static final Logger logger = LoggerFactory.getLogger(OauthController.class); //TODO use right oauth error uri private static final String DEFAULT_ERROR_URI = "https://kaif.io"; @Autowired private ClientAppService clientAppService; @RequestMapping(value = "/authorize", method = RequestMethod.GET) public Object authorize(HttpServletResponse response, @RequestParam(value = "client_id", required = false) String clientId, @RequestParam(value = "state", required = false) String state, @RequestParam(value = "scope", required = false) String scope, @RequestParam(value = "response_type", required = false) String responseType, @RequestParam(value = "redirect_uri", required = false) String redirectUri) { try { Optional<ClientApp> clientApp = clientAppService.verifyRedirectUri(clientId, redirectUri); if (!clientApp.isPresent()) { logger.warn("invalid redirect uri, may be attack: {}, {}", clientId, redirectUri); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return new ModelAndView("error"); } if (!"code".equals(responseType)) { return redirectViewWithError(redirectUri, OauthErrors.CodeResponse.UNSUPPORTED_RESPONSE_TYPE, "response_type must be code", state); } if (Strings.isNullOrEmpty(state)) { return redirectViewWithError(redirectUri, OauthErrors.CodeResponse.INVALID_REQUEST, "missing state", state); } Set<ClientAppScope> clientAppScopes = ClientAppScope.tryParse(scope); if (clientAppScopes.isEmpty()) { return redirectViewWithError(redirectUri, OauthErrors.CodeResponse.INVALID_SCOPE, "wrong scope", state); } //TODO handle error=temporary_unavailable return new ModelAndView("v1/authorize").addObject("clientApp", clientApp.get()) .addObject("clientAppScopes", clientAppScopes); } catch (RuntimeException e) { logger.warn("unexpected error while GET /authorize", e); return redirectViewWithError(redirectUri, OauthErrors.CodeResponse.SERVER_ERROR, "unknown server error", state); } } @RequestMapping(value = "/authorize", method = RequestMethod.POST) public Object grantCode(HttpServletResponse response, @RequestParam(value = "grantDeny", required = false) Boolean grantDeny, @RequestParam(value = "oauthDirectAuthorize") String oauthDirectAuthorize, @RequestParam(value = "client_id") String clientId, @RequestParam(value = "state") String state, @RequestParam(value = "scope") String scope, @RequestParam(value = "redirect_uri") String redirectUri) { setResponseNoCache(response); try { if (Optional.ofNullable(grantDeny).filter(deny -> deny).isPresent()) { throw new AccessDeniedException("user cancel"); } final String code = clientAppService.directGrantCode(oauthDirectAuthorize, clientId, scope, redirectUri); return redirectViewWithQuery(redirectUri, state, "code=" + code); } catch (AccessDeniedException e) { return redirectViewWithError(redirectUri, OauthErrors.CodeResponse.ACCESS_DENIED, "access denied", state); } catch (RuntimeException e) { logger.warn("unexpected error while PUT /authorize", e); return redirectViewWithError(redirectUri, OauthErrors.CodeResponse.SERVER_ERROR, "unknown server error", state); } } private RedirectView redirectViewWithError(String redirectUri, String error, String errorDescription, String state) { String query = String.format("%s=%s&%s=%s&%s=%s", OauthErrors.OAUTH_ERROR, error, OauthErrors.OAUTH_ERROR_DESCRIPTION, errorDescription, OauthErrors.OAUTH_ERROR_URI, DEFAULT_ERROR_URI); return redirectViewWithQuery(redirectUri, state, query); } private RedirectView redirectViewWithQuery(String redirectUri, String state, String query) { try { if (!Strings.isNullOrEmpty(state)) { query += "&state=" + state; } String encoded = UriUtils.encodeQuery(query, Charsets.UTF_8.name()); String locationUri = redirectUri; if (redirectUri.contains("?")) { locationUri += "&" + encoded; } else { locationUri += "?" + encoded; } RedirectView redirectView = new RedirectView(locationUri); redirectView.setStatusCode(HttpStatus.FOUND); redirectView.setExposeModelAttributes(false); redirectView.setPropagateQueryParams(false); return redirectView; } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } /** * spec is POST with application/x-www-form-urlencoded and return JSON * <p> * we allow other `accept` for friendly usage, but may be this is insecure ? */ @RequestMapping(value = "/access-token", method = RequestMethod.POST) @ResponseBody public Object accessToken(HttpServletResponse response, @RequestParam(value = "client_id", required = false) String clientId, @RequestParam(value = "client_secret", required = false) String clientSecret, @RequestParam(value = "grant_type", required = false) String grantType, @RequestParam(value = "code", required = false) String code, @RequestParam(value = "redirect_uri", required = false) String redirectUri) { setResponseNoCache(response); if (!GrantType.AUTHORIZATION_CODE.toString().equals(grantType)) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return new OauthErrorDto(OauthErrors.TokenResponse.UNSUPPORTED_GRANT_TYPE, "grant_type must be authorization_code", DEFAULT_ERROR_URI); } if (Strings.isNullOrEmpty(code)) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return new OauthErrorDto(OauthErrors.TokenResponse.INVALID_REQUEST, "missing code", DEFAULT_ERROR_URI); } if (Strings.isNullOrEmpty(clientId)) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return new OauthErrorDto(OauthErrors.TokenResponse.INVALID_REQUEST, "missing client_id", DEFAULT_ERROR_URI); } if (Strings.isNullOrEmpty(redirectUri)) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return new OauthErrorDto(OauthErrors.TokenResponse.INVALID_REQUEST, "missing redirect_uri", DEFAULT_ERROR_URI); } if (!clientAppService.validateApp(clientId, clientSecret)) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return new OauthErrorDto(OauthErrors.TokenResponse.INVALID_CLIENT, "invalid client", DEFAULT_ERROR_URI); } try { return clientAppService.createOauthAccessTokenByGrantCode(code, clientId, redirectUri); } catch (AccessDeniedException e) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return new OauthErrorDto(OauthErrors.TokenResponse.INVALID_GRANT, "code is invalid", DEFAULT_ERROR_URI); } } void setResponseNoCache(HttpServletResponse response) { response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store"); response.setHeader(HttpHeaders.PRAGMA, "no-cache"); } }