001/*
002 * Copyright (c) 2017 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: crawl $'
006 * '$Date: 2017-07-04 14:25:38 -0700 (Tue, 04 Jul 2017) $' 
007 * '$Revision: 1257 $'
008 * 
009 * Permission is hereby granted, without written agreement and without
010 * license or royalty fees, to use, copy, modify, and distribute this
011 * software and its documentation for any purpose, provided that the above
012 * copyright notice and the following two paragraphs appear in all copies
013 * of this software.
014 *
015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
019 * SUCH DAMAGE.
020 *
021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
026 * ENHANCEMENTS, OR MODIFICATIONS.
027 *
028 */
029package org.kepler.webview.server.auth;
030
031import java.net.HttpURLConnection;
032
033import org.kepler.webview.server.WebViewServer;
034
035import io.vertx.core.AsyncResult;
036import io.vertx.core.Future;
037import io.vertx.core.Handler;
038import io.vertx.core.http.HttpClient;
039import io.vertx.core.http.HttpClientOptions;
040import io.vertx.core.http.HttpClientRequest;
041import io.vertx.core.json.JsonObject;
042import io.vertx.ext.auth.AuthProvider;
043import io.vertx.ext.auth.User;
044
045/** AuthProvider that authenticates against a Drupal site.
046 * 
047 * @author Daniel Crawl
048 * @version $Id: DrupalAuth.java 1257 2017-07-04 21:25:38Z crawl $
049 * 
050 */
051public class DrupalAuth implements AuthProvider {
052
053    /** Create a new DrupalAuth.
054     *  @param host The hostname of the drupal site.
055     *  @param service The name of the Services REST service.
056     *  @param role The name of the role required by authorized users.
057     *  @param groupsField The name of the user field containing the group name(s).
058     */
059    public DrupalAuth(String host, String service, String role, String groupsField,
060        String fullNameField) {
061
062        _loginUrl = "?q=" + service + "/user/login.json";
063        _logoutUrl = "?q=" + service + "/user/logout.json";
064        _role = role;
065        _groupsField = "field_" + groupsField;
066        _fullNameField = "field_" + fullNameField;
067        
068        _client = WebViewServer.vertx().createHttpClient(
069            new HttpClientOptions().setDefaultHost(host)
070                .setDefaultPort(443).setSsl(true).setTrustAll(true));//.setLogActivity(true));
071    }
072    
073    @Override
074    public void authenticate(JsonObject authInfo, Handler<AsyncResult<User>> handler) {
075        
076        //System.out.println("authenticating for " + authInfo.getString("username"));
077        
078        String username = authInfo.getString("username");
079        if (username == null) {
080            handler.handle(Future.failedFuture("authInfo must contain username in 'username' field"));
081        } else {
082            String password = authInfo.getString("password");
083            if (password == null) {
084                handler.handle(Future.failedFuture("authInfo must contain password in 'password' field"));
085            } else {
086                //System.out.println(_sessionTokenUrl);
087                _getCSRFToken(null, loginTokenRes -> {
088                    if(loginTokenRes.failed()) {
089                        handler.handle(Future.failedFuture(loginTokenRes.cause().getMessage()));
090                    } else {
091                        //String body = "{username=\"" + username + "\",password=\"" + password + "\"}";
092                        String body = new JsonObject().put("username", username).put("password", password).encode();
093                        //System.out.println(body);
094                        _client.post(_loginUrl, loginResponse -> {
095                            //System.out.println("login status " + loginResponse.statusCode());
096                            //System.out.println("login message " + loginResponse.statusMessage());
097                            if(loginResponse.statusCode() != HttpURLConnection.HTTP_OK) {
098                                String message = loginResponse.statusMessage();
099                                if(message.indexOf(": ") > -1) {
100                                    message = message.substring(message.indexOf(": ") + 2);
101                                }
102                                handler.handle(Future.failedFuture(message));
103                            } else {
104                                loginResponse.handler(loginBuffer -> {
105                                    JsonObject loginJson = loginBuffer.toJsonObject();
106                                    User user = new DrupalUser(loginJson, _groupsField, _fullNameField, _role);
107                                    handler.handle(Future.succeededFuture(user));
108
109                                    String sessionCookieStr = loginJson.getString("session_name") + "=" + loginJson.getString("sessid");                                    
110                                    _getCSRFToken(sessionCookieStr, logoutTokenRes -> {
111                                        if(logoutTokenRes.failed()) {
112                                            System.err.println("WARNING: could not get CSRF token for logout: " +
113                                                loginTokenRes.cause().getMessage());
114                                        } else {
115                                            _client.post(_logoutUrl, logoutResponse -> {
116                                                //System.out.println("logout: " + logoutResponse.statusMessage());
117                                                if(logoutResponse.statusCode() != HttpURLConnection.HTTP_OK) {
118                                                    System.err.println("WARNING: could not log out: " + logoutResponse.statusMessage());
119                                                    System.out.println("Cookie" + loginJson.getString("session_name") + "=" + loginJson.getString("sessid"));
120                                                }
121                                            }).putHeader("X-CSRF-Token", logoutTokenRes.result())
122                                                .putHeader("Cookie", sessionCookieStr)
123                                                .putHeader("Content-Type", "application/json")
124                                                .putHeader("Content-Length", String.valueOf(0))
125                                                .end();
126                                        }
127                                    });
128                                });
129                            }
130                        }).putHeader("X-CSRF-Token", loginTokenRes.result())
131                            .putHeader("Content-Type", "application/json")
132                            .putHeader("Content-Length", String.valueOf(body.length()))
133                            .write(body)
134                            .end();
135                    }
136                });
137            }
138        }
139
140    }
141        
142    private void _getCSRFToken(String sessionCookieStr, Handler<AsyncResult<String>> handler) {
143        HttpClientRequest request = _client.get(_sessionTokenUrl, response -> {
144            if(response.statusCode() != HttpURLConnection.HTTP_OK) {
145                handler.handle(Future.failedFuture("Failed to get session token while authenticating."));
146            } else {
147                response.handler(buffer -> {
148                    handler.handle(Future.succeededFuture(buffer.toString()));
149                });
150            }
151        });
152        
153        if(sessionCookieStr != null) {
154            request.putHeader("Cookie", sessionCookieStr);
155        }
156
157        request.end();
158    }
159    
160    /** HTTP client to make requests from drupal. */
161    private HttpClient _client;
162    
163    /** REST login url */
164    private String _loginUrl;
165    
166    /** REST logout url */
167    private String _logoutUrl;
168    
169    /** Role user must have */
170    private String _role;
171    
172    /** Name of drupal user field that contains the user's group name(s). */
173    private String _groupsField;
174
175    private String _fullNameField;
176    
177    /** REST get session token url. */
178    private final static String _sessionTokenUrl = "/?q=services/session/token";
179
180}