001/*
002 * (C) Copyright 2006-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Antoine Taillefer
018 */
019package org.nuxeo.ecm.tokenauth.servlet;
020
021import static java.nio.charset.StandardCharsets.UTF_8;
022
023import java.io.IOException;
024import java.io.OutputStream;
025import java.security.Principal;
026
027import javax.servlet.ServletException;
028import javax.servlet.http.HttpServlet;
029import javax.servlet.http.HttpServletRequest;
030import javax.servlet.http.HttpServletResponse;
031
032import org.apache.commons.lang3.StringUtils;
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.apache.http.HttpStatus;
036import org.nuxeo.ecm.core.api.NuxeoPrincipal;
037import org.nuxeo.ecm.platform.ui.web.auth.service.AuthenticationPluginDescriptor;
038import org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService;
039import org.nuxeo.ecm.platform.ui.web.auth.token.TokenAuthenticator;
040import org.nuxeo.ecm.tokenauth.TokenAuthenticationException;
041import org.nuxeo.ecm.tokenauth.service.TokenAuthenticationService;
042import org.nuxeo.runtime.api.Framework;
043
044/**
045 * Servlet that allows to get a unique authentication token given the request Principal and some device information
046 * passed as request parameters: application name, device id, device description, permission. An error response will be
047 * sent with a 400 status code if one of the required parameters is null or empty. All parameters are required except
048 * for the device description.
049 * <p>
050 * The token is provided by the {@link TokenAuthenticationService}.
051 *
052 * @author Antoine Taillefer (ataillefer@nuxeo.com)
053 * @since 5.7
054 */
055public class TokenAuthenticationServlet extends HttpServlet {
056
057    private static final long serialVersionUID = 7792388601558509103L;
058
059    private static final Log log = LogFactory.getLog(TokenAuthenticationServlet.class);
060
061    public static final String TOKEN_AUTH_PLUGIN_NAME = "TOKEN_AUTH";
062
063    public static final String APPLICATION_NAME_PARAM = "applicationName";
064
065    public static final String DEVICE_ID_PARAM = "deviceId";
066
067    public static final String DEVICE_DESCRIPTION_PARAM = "deviceDescription";
068
069    public static final String PERMISSION_PARAM = "permission";
070
071    public static final String REVOKE_PARAM = "revoke";
072
073    @Override
074    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
075
076        // Don't provide token for anonymous user unless 'allowAnonymous' parameter is explicitly set to true in
077        // the authentication plugin configuration
078        Principal principal = req.getUserPrincipal();
079        if (principal instanceof NuxeoPrincipal && ((NuxeoPrincipal) principal).isAnonymous()) {
080            PluggableAuthenticationService authenticationService = (PluggableAuthenticationService) Framework.getRuntime()
081                                                                                                             .getComponent(
082                                                                                                                     PluggableAuthenticationService.NAME);
083            AuthenticationPluginDescriptor tokenAuthPluginDesc = authenticationService.getDescriptor(
084                    TOKEN_AUTH_PLUGIN_NAME);
085            if (tokenAuthPluginDesc == null || !(Boolean.parseBoolean(
086                    tokenAuthPluginDesc.getParameters().get(TokenAuthenticator.ALLOW_ANONYMOUS_KEY)))) {
087                log.debug("Anonymous user is not allowed to acquire an authentication token.");
088                resp.sendError(HttpStatus.SC_UNAUTHORIZED);
089                return;
090            }
091
092        }
093
094        // Get request parameters
095        String applicationName = req.getParameter(APPLICATION_NAME_PARAM);
096        String deviceId = req.getParameter(DEVICE_ID_PARAM);
097        String deviceDescription = req.getParameter(DEVICE_DESCRIPTION_PARAM);
098        String permission = req.getParameter(PERMISSION_PARAM);
099        String revokeParam = req.getParameter(REVOKE_PARAM);
100        boolean revoke = Boolean.parseBoolean(revokeParam);
101
102        // If one of the required parameters is null or empty, send an
103        // error with the 400 status
104        if (!revoke && (StringUtils.isEmpty(applicationName) || StringUtils.isEmpty(deviceId)
105                || StringUtils.isEmpty(permission))) {
106            log.error(
107                    "The following request parameters are mandatory to acquire an authentication token: applicationName, deviceId, permission.");
108            resp.sendError(HttpStatus.SC_BAD_REQUEST);
109            return;
110        }
111        if (revoke && (StringUtils.isEmpty(applicationName) || StringUtils.isEmpty(deviceId))) {
112            log.error(
113                    "The following request parameters are mandatory to revoke an authentication token: applicationName, deviceId.");
114            resp.sendError(HttpStatus.SC_BAD_REQUEST);
115            return;
116        }
117
118        // Get user name from request Principal
119        if (principal == null) {
120            resp.sendError(HttpStatus.SC_UNAUTHORIZED);
121            return;
122        }
123        String userName = principal.getName();
124
125        // Write response
126        String response;
127        int statusCode;
128        TokenAuthenticationService tokenAuthService = Framework.getService(TokenAuthenticationService.class);
129        try {
130            // Token acquisition: acquire token and write it to the response
131            // body
132            if (!revoke) {
133                response = tokenAuthService.acquireToken(userName, applicationName, deviceId, deviceDescription,
134                        permission);
135                statusCode = 201;
136            }
137            // Token revocation
138            else {
139                String token = tokenAuthService.getToken(userName, applicationName, deviceId);
140                if (token == null) {
141                    response = String.format(
142                            "No token found for userName %s, applicationName %s and deviceId %s; nothing to do.",
143                            userName, applicationName, deviceId);
144                    statusCode = 400;
145                } else {
146                    tokenAuthService.revokeToken(token);
147                    response = String.format("Token revoked for userName %s, applicationName %s and deviceId %s.",
148                            userName, applicationName, deviceId);
149                    statusCode = 202;
150                }
151            }
152            sendTextResponse(resp, response, statusCode);
153        } catch (TokenAuthenticationException e) {
154            // Should never happen as parameters have already been checked
155            resp.sendError(HttpStatus.SC_NOT_FOUND);
156        }
157    }
158
159    protected void sendTextResponse(HttpServletResponse resp, String textResponse, int statusCode) throws IOException {
160
161        resp.setContentType("text/plain");
162        resp.setCharacterEncoding(UTF_8.name());
163        resp.setStatus(statusCode);
164        resp.setContentLength(textResponse.getBytes().length);
165        OutputStream out = resp.getOutputStream();
166        out.write(textResponse.getBytes(UTF_8));
167        out.close();
168    }
169
170}