/** * Copyright © 2015 Instituto Superior Técnico * * This file is part of Bennu OAuth. * * Bennu OAuth is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Bennu OAuth 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. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Bennu OAuth. If not, see <http://www.gnu.org/licenses/>. */ package org.fenixedu.bennu.oauth.servlets; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.Reader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response.Status; import org.fenixedu.bennu.core.domain.User; import org.fenixedu.bennu.core.i18n.BundleUtil; import org.fenixedu.bennu.core.security.Authenticate; import org.fenixedu.bennu.core.util.CoreConfiguration; import org.fenixedu.bennu.oauth.domain.ApplicationUserAuthorization; import org.fenixedu.bennu.oauth.domain.ApplicationUserSession; import org.fenixedu.bennu.oauth.domain.ExternalApplication; import org.fenixedu.bennu.oauth.domain.ServiceApplication; import org.fenixedu.bennu.oauth.util.OAuthUtils; import org.fenixedu.bennu.portal.BennuPortalConfiguration; import org.fenixedu.bennu.portal.domain.PortalConfiguration; import org.fenixedu.commons.i18n.I18N; import pt.ist.fenixframework.Atomic; import pt.ist.fenixframework.FenixFramework; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.net.HttpHeaders; import com.google.gson.JsonObject; import com.mitchellbosecke.pebble.PebbleEngine; import com.mitchellbosecke.pebble.PebbleEngine.Builder; import com.mitchellbosecke.pebble.error.LoaderException; import com.mitchellbosecke.pebble.error.PebbleException; import com.mitchellbosecke.pebble.extension.AbstractExtension; import com.mitchellbosecke.pebble.extension.Function; import com.mitchellbosecke.pebble.loader.ClasspathLoader; import com.mitchellbosecke.pebble.template.PebbleTemplate; /** * Servlet implementation class OAuthAuthorizationServlet */ @WebServlet("/oauth/*") public class OAuthAuthorizationServlet extends HttpServlet { private static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials"; private static final String GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code"; private static final String CODE_EXPIRED = "code expired"; private static final String CODE_INVALID = "code invalid"; private static final long serialVersionUID = 1L; private final static String OAUTH_SESSION_KEY = "OAUTH_CLIENT_ID"; private final static String CLIENT_ID = "client_id"; private final static String CLIENT_SECRET = "client_secret"; private final static String REDIRECT_URI = "redirect_uri"; private final static String CODE = "code"; private final static String GRANT_TYPE = "grant_type"; private final static String DEVICE_ID = "device_id"; private final static String INVALID_GRANT = "invalid_grant"; private static final String REFRESH_TOKEN_DOESN_T_MATCH = "refresh token doesn't match"; private static final String CREDENTIALS_OR_REDIRECT_URI_DON_T_MATCH = "credentials or redirect_uri don't match"; private static final String REFRESH_TOKEN_NOT_RECOGNIZED = "refresh token not recognized."; private static final String REFRESH_TOKEN_INVALID = "refreshTokenInvalid"; private static final String REFRESH_TOKEN_INVALID_FORMAT = "refreshTokenInvalidFormat"; private static final String CLIENT_ID_NOT_FOUND = "client_id not found"; private static final String APPLICATION_BANNED = "the application has been banned."; private static final String APPLICATION_DELETED = "the application has been deleted."; private PebbleEngine engine; @Override public void init(ServletConfig config) throws ServletException { super.init(config); engine = new Builder().loader(new ClasspathLoader() { @Override public Reader getReader(String pageName) throws LoaderException { InputStream stream = config.getServletContext().getResourceAsStream( "/themes/" + PortalConfiguration.getInstance().getTheme() + "/oauth/" + pageName + ".html"); if (stream != null) { return new InputStreamReader(stream, StandardCharsets.UTF_8); } else { // ... and fall back if none is provided. return new InputStreamReader(config.getServletContext().getResourceAsStream( "/bennu-oauth/" + pageName + ".html"), StandardCharsets.UTF_8); } } }).cacheActive(!BennuPortalConfiguration.getConfiguration().themeDevelopmentMode()).extension(new AbstractExtension() { @Override public Map<String, Function> getFunctions() { Map<String, Function> functions = new HashMap<>(); functions.put("i18n", new I18NFunction()); return functions; } }).build(); } @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (Strings.isNullOrEmpty(request.getPathInfo())) { response.sendError(404); return; } String path = trim(request.getPathInfo()); switch (path) { case "userdialog": handleUserDialog(request, response); break; case "userconfirmation": if (!"POST".equals(request.getMethod())) { response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); return; } userConfirmation(request, response); break; case OAuthUtils.ACCESS_TOKEN: if (!"POST".equals(request.getMethod())) { response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); return; } handleAccessToken(request, response); break; case OAuthUtils.REFRESH_TOKEN: if (!"POST".equals(request.getMethod())) { response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); return; } handleRefreshToken(request, response); break; default: response.sendError(HttpServletResponse.SC_NOT_FOUND); } } //refreshAccessToken private void handleRefreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException { String[] authorizationHeader = getAuthorizationHeader(request); String clientId; String clientSecret; if (authorizationHeader == null) { clientId = request.getParameter(CLIENT_ID); clientSecret = request.getParameter(CLIENT_SECRET); } else { clientId = authorizationHeader[0]; clientSecret = authorizationHeader[1]; } String refreshToken = request.getParameter(OAuthUtils.REFRESH_TOKEN); ExternalApplication externalApplication = OAuthUtils.getDomainObject(clientId, ExternalApplication.class).orElse(null); if (externalApplication == null) { sendOAuthErrorResponse(response, Status.BAD_REQUEST, INVALID_GRANT, CLIENT_ID_NOT_FOUND); return; } if (!isValidApplication(response, externalApplication)) { // this method sends error response if needed return; } if (Strings.isNullOrEmpty(refreshToken)) { sendOAuthErrorResponse(response, Status.UNAUTHORIZED, REFRESH_TOKEN_INVALID_FORMAT, REFRESH_TOKEN_NOT_RECOGNIZED); return; } String refreshTokenDecoded; try { refreshTokenDecoded = new String(Base64.getDecoder().decode(refreshToken), StandardCharsets.UTF_8); } catch (IllegalArgumentException iae) { sendOAuthErrorResponse(response, Status.UNAUTHORIZED, REFRESH_TOKEN_INVALID_FORMAT, REFRESH_TOKEN_NOT_RECOGNIZED); return; } String[] refreshTokenBuilder = refreshTokenDecoded.split(":"); if (refreshTokenBuilder.length != 2) { sendOAuthErrorResponse(response, Status.UNAUTHORIZED, REFRESH_TOKEN_INVALID_FORMAT, REFRESH_TOKEN_NOT_RECOGNIZED); return; } String appUserSessionExternalId = refreshTokenBuilder[0]; ApplicationUserSession appUserSession = FenixFramework.getDomainObject(appUserSessionExternalId); if (!externalApplication.matchesSecret(clientSecret)) { sendOAuthErrorResponse(response, Status.UNAUTHORIZED, INVALID_GRANT, CREDENTIALS_OR_REDIRECT_URI_DON_T_MATCH); return; } if (!FenixFramework.isDomainObjectValid(appUserSession) || !appUserSession.matchesRefreshToken(refreshToken)) { sendOAuthErrorResponse(response, Status.UNAUTHORIZED, REFRESH_TOKEN_INVALID, REFRESH_TOKEN_DOESN_T_MATCH); return; } if (appUserSession.getApplicationUserAuthorization().getUser().isLoginExpired()) { sendOAuthErrorResponse(response, Status.BAD_REQUEST, REFRESH_TOKEN_INVALID_FORMAT, REFRESH_TOKEN_NOT_RECOGNIZED); return; } String newAccessToken = OAuthUtils.generateToken(appUserSession); appUserSession.setNewAccessToken(newAccessToken); sendOAuthResponse(response, Status.OK, OAuthUtils.getJsonTokens(appUserSession)); } private String[] getAuthorizationHeader(HttpServletRequest request) { String authorization = request.getHeader("Authorization"); if (authorization != null && authorization.startsWith("Basic")) { String base64Credentials = authorization.substring("Basic".length()).trim(); String[] values; try { String credentials = new String(Base64.getDecoder().decode(base64Credentials), Charset.forName("UTF-8")); values = credentials.split(":", 2); } catch (IllegalArgumentException iae) { return null; } return values.length != 2 ? null : values; } return null; } //getTokens private void handleAccessToken(HttpServletRequest request, HttpServletResponse response) throws IOException { String[] authorizationHeader = getAuthorizationHeader(request); String clientId; String clientSecret; if (authorizationHeader == null) { clientId = request.getParameter(CLIENT_ID); clientSecret = request.getParameter(CLIENT_SECRET); } else { clientId = authorizationHeader[0]; clientSecret = authorizationHeader[1]; } String redirectUrl = request.getParameter(REDIRECT_URI); String authCode = request.getParameter(CODE); String grantType = request.getParameter(GRANT_TYPE); if (Strings.isNullOrEmpty(clientId) || Strings.isNullOrEmpty(clientSecret) || Strings.isNullOrEmpty(grantType)) { sendOAuthErrorResponse(response, Status.BAD_REQUEST, INVALID_GRANT, Joiner.on(",").join(CLIENT_ID, CLIENT_SECRET, GRANT_TYPE) + " are mandatory"); return; } if (!GRANT_TYPE_AUTHORIZATION_CODE.equals(grantType) && !GRANT_TYPE_CLIENT_CREDENTIALS.equals(grantType)) { sendOAuthErrorResponse( response, Status.BAD_REQUEST, INVALID_GRANT, GRANT_TYPE + " must be on of the following values: " + Joiner.on(",").join(GRANT_TYPE_CLIENT_CREDENTIALS, GRANT_TYPE_AUTHORIZATION_CODE)); return; } if (GRANT_TYPE_AUTHORIZATION_CODE.equals(grantType)) { if (Strings.isNullOrEmpty(redirectUrl) || Strings.isNullOrEmpty(authCode)) { sendOAuthErrorResponse(response, Status.BAD_REQUEST, INVALID_GRANT, Joiner.on(",").join(REDIRECT_URI, CODE) + " are mandatory"); return; } } ExternalApplication externalApplication = OAuthUtils.getDomainObject(clientId, ExternalApplication.class).orElse(null); if (externalApplication == null) { sendOAuthErrorResponse(response, Status.BAD_REQUEST, INVALID_GRANT, CLIENT_ID_NOT_FOUND); return; } if (externalApplication instanceof ServiceApplication && !GRANT_TYPE_CLIENT_CREDENTIALS.equals(grantType)) { sendOAuthErrorResponse(response, Status.BAD_REQUEST, INVALID_GRANT, CLIENT_ID_NOT_FOUND); return; } if (!isValidApplication(response, externalApplication)) { // this method sends error response if needed return; } if (!externalApplication.matches(redirectUrl, clientSecret)) { sendOAuthErrorResponse(response, Status.BAD_REQUEST, INVALID_GRANT, CREDENTIALS_OR_REDIRECT_URI_DON_T_MATCH); return; } if (externalApplication instanceof ServiceApplication) { final String accessToken = OAuthUtils.generateToken(externalApplication); ((ServiceApplication) externalApplication).createServiceAuthorization(accessToken); sendOAuthResponse(response, Status.OK, OAuthUtils.getJsonTokens(accessToken)); return; } ApplicationUserSession appUserSession = externalApplication.getApplicationUserSession(authCode); if (appUserSession == null) { sendOAuthErrorResponse(response, Status.BAD_REQUEST, INVALID_GRANT, CODE_INVALID); return; } if (appUserSession.getApplicationUserAuthorization().getUser().isLoginExpired()) { sendOAuthErrorResponse(response, Status.BAD_REQUEST, INVALID_GRANT, CODE_EXPIRED); return; } if (appUserSession.isCodeValid()) { String accessToken = OAuthUtils.generateToken(appUserSession); String refreshToken = OAuthUtils.generateToken(appUserSession); appUserSession.setTokens(accessToken, refreshToken); sendOAuthResponse(response, Status.OK, OAuthUtils.getJsonTokens(appUserSession)); } else { sendOAuthErrorResponse(response, Status.BAD_REQUEST, INVALID_GRANT, CODE_EXPIRED); } } private void handleUserDialog(HttpServletRequest request, HttpServletResponse response) throws IOException { String clientId = request.getParameter(CLIENT_ID); String redirectUrl = request.getParameter(REDIRECT_URI); User user = Authenticate.getUser(); if (!Strings.isNullOrEmpty(clientId) && !Strings.isNullOrEmpty(redirectUrl)) { if (user == null) { final String cookieValue = clientId + "|" + redirectUrl; response.addCookie(new Cookie(OAUTH_SESSION_KEY, Base64.getEncoder().encodeToString(cookieValue.getBytes(StandardCharsets.UTF_8)))); response.sendRedirect(request.getContextPath() + "/login?callback=" + CoreConfiguration.getConfiguration().applicationUrl() + "/oauth/userdialog"); return; } else { redirectToRedirectUrl(request, response, user, clientId, redirectUrl); return; } } else { if (user != null) { final Cookie cookie = getOAuthSessionCookie(request); if (cookie == null) { errorPage(request, response); return; } final String sessionClientId = cookie.getValue(); if (!Strings.isNullOrEmpty(sessionClientId)) { redirectToRedirectUrl(request, response, user, cookie); return; } } } errorPage(request, response); } private static Cookie getOAuthSessionCookie(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookie.getName().equalsIgnoreCase(OAUTH_SESSION_KEY)) { return cookie; } } } return null; } private void errorPage(HttpServletRequest request, HttpServletResponse response) throws IOException { Map<String, Object> ctx = new HashMap<>(); PortalConfiguration config = PortalConfiguration.getInstance(); // Add relevant variables ctx.put("config", config); ctx.put("currentLocale", I18N.getLocale()); ctx.put("contextPath", request.getContextPath()); ctx.put("locales", CoreConfiguration.supportedLocales()); try { response.setContentType("text/html;charset=UTF-8"); PebbleTemplate template = engine.getTemplate("error-page"); template.evaluate(response.getWriter(), ctx, I18N.getLocale()); } catch (PebbleException e) { throw new IOException(e); } } private static class I18NFunction implements Function { final List<String> variableArgs = Stream.of("arg0", "arg1", "arg2", "arg3", "arg4", "arg5").collect(Collectors.toList()); @Override public List<String> getArgumentNames() { return Stream.of("bundle", "key", "arg0", "arg1", "arg2", "arg3", "arg4", "arg5").collect(Collectors.toList()); } @Override public Object execute(Map<String, Object> args) { String bundle = (String) args.get("bundle"); String key = args.get("key").toString(); return BundleUtil.getString(bundle, key, arguments(args)); } public String[] arguments(Map<String, Object> args) { List<String> values = new ArrayList<>(); for (String variableArg : variableArgs) { if (args.containsKey(variableArg) && args.get(variableArg) instanceof String) { values.add((String) args.get(variableArg)); } } return values.toArray(new String[] {}); } } private void authorizationPage(HttpServletRequest request, HttpServletResponse response, ExternalApplication clientApplication) throws IOException { Map<String, Object> ctx = new HashMap<>(); PortalConfiguration config = PortalConfiguration.getInstance(); // Add relevant variables ctx.put("config", config); ctx.put("app", clientApplication); ctx.put("currentLocale", I18N.getLocale()); ctx.put("contextPath", request.getContextPath()); ctx.put("locales", CoreConfiguration.supportedLocales()); ctx.put("loggedUser", Authenticate.getUser()); try { response.setContentType("text/html;charset=UTF-8"); PebbleTemplate template = engine.getTemplate("auth-page"); template.evaluate(response.getWriter(), ctx, I18N.getLocale()); } catch (PebbleException e) { throw new IOException(e); } } private void redirectToRedirectUrl(HttpServletRequest request, HttpServletResponse response, User user, final Cookie cookie) throws IOException { String cookieValue = new String(Base64.getDecoder().decode(cookie.getValue()), StandardCharsets.UTF_8); final int indexOf = cookieValue.indexOf("|"); String clientApplicationId = cookieValue.substring(0, indexOf); String redirectUrl = cookieValue.substring(indexOf + 1, cookieValue.length()); redirectToRedirectUrl(request, response, user, clientApplicationId, redirectUrl); } private void redirectToRedirectUrl(HttpServletRequest request, HttpServletResponse response, User user, String clientId, String redirectUrl) throws IOException { ExternalApplication externalApplication = OAuthUtils.getDomainObject(clientId, ExternalApplication.class).orElse(null); if (externalApplication == null || externalApplication instanceof ServiceApplication) { sendOAuthErrorResponse(response, Status.BAD_REQUEST, INVALID_GRANT, CLIENT_ID_NOT_FOUND); return; } if (!isValidApplication(response, externalApplication)) { // this method sends error response if needed return; } if (!externalApplication.matchesUrl(redirectUrl)) { sendOAuthErrorResponse(response, Status.BAD_REQUEST, INVALID_GRANT, CREDENTIALS_OR_REDIRECT_URI_DON_T_MATCH); return; } if (!externalApplication.hasApplicationUserAuthorization(user)) { request.setAttribute("application", externalApplication); authorizationPage(request, response, externalApplication); return; } else { redirectWithCode(request, response, user, externalApplication); return; } } private void redirectWithCode(HttpServletRequest request, HttpServletResponse response, User user, ExternalApplication clientApplication) throws IOException { final String code = createAppUserSession(clientApplication, user, request, response); response.sendRedirect(clientApplication.getRedirectUrl() + "?" + CODE + "=" + code); } public void userConfirmation(HttpServletRequest request, HttpServletResponse response) throws IOException { User user = Authenticate.getUser(); if (user == null) { errorPage(request, response); return; } String clientId = request.getParameter(CLIENT_ID); String redirectUrl = request.getParameter(REDIRECT_URI); ExternalApplication externalApplication = (ExternalApplication) OAuthUtils.getDomainObject(clientId).orElse(null); if (externalApplication == null || externalApplication instanceof ServiceApplication) { sendOAuthErrorResponse(response, Status.BAD_REQUEST, INVALID_GRANT, CLIENT_ID_NOT_FOUND); return; } if (!isValidApplication(response, externalApplication)) { // this method sends error response if needed return; } if (externalApplication.matchesUrl(redirectUrl)) { redirectWithCode(request, response, user, externalApplication); return; } errorPage(request, response); } private boolean isValidApplication(HttpServletResponse response, ExternalApplication clientApplication) { if (clientApplication.isDeleted()) { sendOAuthErrorResponse(response, Status.UNAUTHORIZED, INVALID_GRANT, APPLICATION_DELETED); return false; } if (clientApplication.isBanned()) { sendOAuthErrorResponse(response, Status.UNAUTHORIZED, INVALID_GRANT, APPLICATION_BANNED); return false; } return true; } @Atomic private static String createAppUserSession(ExternalApplication application, User user, HttpServletRequest request, HttpServletResponse response) { String code = OAuthUtils.generateCode(); ApplicationUserAuthorization appUserAuthorization = application.getApplicationUserAuthorization(user).orElseGet( () -> new ApplicationUserAuthorization(user, application)); ApplicationUserSession appUserSession = new ApplicationUserSession(); appUserSession.setCode(code); appUserSession.setDeviceId(getDeviceId(request)); appUserSession.setApplicationUserAuthorization(appUserAuthorization); return code; } private void sendOAuthErrorResponse(HttpServletResponse response, Status status, String error, String errorDescription) { JsonObject errorResponse = new JsonObject(); errorResponse.addProperty("error", error); errorResponse.addProperty("errorDescription", errorDescription); sendOAuthResponse(response, status, errorResponse); } private void sendOAuthResponse(HttpServletResponse response, Status status, JsonObject jsonResponse) { response.setContentType("application/json; charset=UTF-8"); response.setStatus(status.getStatusCode()); try (PrintWriter pw = response.getWriter()) { pw.print(jsonResponse.toString()); pw.flush(); } catch (IOException e) { throw new WebApplicationException(e); } } private static String getDeviceId(HttpServletRequest request) { String deviceId = request.getParameter(DEVICE_ID); if (Strings.isNullOrEmpty(deviceId)) { return request.getHeader(HttpHeaders.USER_AGENT); } return deviceId; } private String trim(String value) { int len = value.length(); int st = 0; char[] val = value.toCharArray(); while ((st < len) && (val[st] == '/')) { st++; } while ((st < len) && (val[len - 1] == '/')) { len--; } return ((st > 0) || (len < value.length())) ? value.substring(st, len) : value; } }