org.entcore.auth.oauth.OAuthDataHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.entcore.auth.oauth.OAuthDataHandler.java

Source

/* Copyright  "Open Digital Education", 2014
 *
 * This program is published by "Open Digital Education".
 * You must indicate the name of the software and the company in any production /contribution
 * using the software and indicate on the home page of the software industry in question,
 * "powered by Open Digital Education" with a reference to the website: https://opendigitaleducation.com/.
 *
 * This program is free software, licensed under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation, version 3 of the License.
 *
 * You can redistribute this application and/or modify it since you respect the terms of the GNU Affero General Public License.
 * If you modify the source code and then use this modified source code in your creation, you must make available the source code of your modifications.
 *
 * You should have received a copy of the GNU Affero General Public License along with the software.
 * If not, please see : <http://www.gnu.org/licenses/>. Full compliance requires reading the terms of this license and following its directives.
    
 *
 */

package org.entcore.auth.oauth;

import com.fasterxml.jackson.databind.ObjectMapper;
import fr.wseduc.mongodb.MongoDb;
import fr.wseduc.webutils.security.BCrypt;
import fr.wseduc.webutils.security.Md5;
import fr.wseduc.webutils.security.Sha256;
import jp.eisbahn.oauth2.server.async.Handler;
import jp.eisbahn.oauth2.server.data.DataHandler;
import jp.eisbahn.oauth2.server.models.AccessToken;
import jp.eisbahn.oauth2.server.models.AuthInfo;
import jp.eisbahn.oauth2.server.models.Request;
import org.entcore.auth.services.OpenIdConnectService;
import org.entcore.common.neo4j.Neo4j;
import io.vertx.core.AsyncResult;
import io.vertx.core.eventbus.Message;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;

import static fr.wseduc.webutils.Utils.getOrElse;

import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.*;

import static fr.wseduc.webutils.Utils.isNotEmpty;

public class OAuthDataHandler extends DataHandler {
    private final Neo4j neo;
    private final MongoDb mongo;
    private final OpenIdConnectService openIdConnectService;
    private final boolean checkFederatedLogin;
    private static final String AUTH_INFO_COLLECTION = "authorizations";
    private static final String ACCESS_TOKEN_COLLECTION = "tokens";
    private static final int CODE_EXPIRES = 600000; // 10 min
    private static final Logger log = LoggerFactory.getLogger(OAuthDataHandler.class);

    public OAuthDataHandler(Request request, Neo4j neo, MongoDb mongo, OpenIdConnectService openIdConnectService,
            boolean checkFederatedLogin) {
        super(request);
        this.neo = neo;
        this.mongo = mongo;
        this.openIdConnectService = openIdConnectService;
        this.checkFederatedLogin = checkFederatedLogin;
    }

    @Override
    public void validateClient(String clientId, String clientSecret, String grantType,
            final Handler<Boolean> handler) {

        String query = "MATCH (n:Application) " + "WHERE n.name = {clientId} " + "AND n.secret = {secret} ";
        if (!"refresh_token".equals(grantType)) {
            query += " AND n.grantType = {grantType} ";
        }
        query += "RETURN count(n) as nb";
        Map<String, Object> params = new HashMap<>();
        params.put("clientId", clientId);
        params.put("secret", clientSecret);
        params.put("grantType", grantType);
        neo.execute(query, params, new io.vertx.core.Handler<Message<JsonObject>>() {
            @Override
            public void handle(Message<JsonObject> res) {
                JsonArray a = res.body().getJsonArray("result");
                if ("ok".equals(res.body().getString("status")) && a != null && a.size() == 1) {
                    JsonObject r = a.getJsonObject(0);
                    handler.handle(r != null && r.getInteger("nb") == 1);
                } else {
                    handler.handle(false);
                }
            }
        });

    }

    @Override
    public void getUserId(final String username, final String password, final Handler<String> handler) {
        if (username != null && password != null && !username.trim().isEmpty() && !password.trim().isEmpty()) {
            String query = "MATCH (n:User) " + "WHERE n.login={login} AND NOT(n.password IS NULL) "
                    + "AND (NOT(HAS(n.blocked)) OR n.blocked = false) ";
            if (checkFederatedLogin) {
                query += "AND (NOT(HAS(n.federated)) OR n.federated = false) ";
            }
            query += "OPTIONAL MATCH (p:Profile) " + "WHERE HAS(n.profiles) AND p.name = head(n.profiles) "
                    + "RETURN DISTINCT n.id as userId, n.password as password, p.blocked as blockedProfile";
            Map<String, Object> params = new HashMap<>();
            params.put("login", username);
            neo.execute(query, params, new io.vertx.core.Handler<Message<JsonObject>>() {

                @Override
                public void handle(Message<JsonObject> res) {
                    JsonArray result = res.body().getJsonArray("result");
                    if ("ok".equals(res.body().getString("status")) && result != null && result.size() == 1) {
                        checkPassword(result, password, username, handler);
                    } else {
                        getUserIdByLoginAlias(username, password, handler);
                    }
                }
            });
        } else {
            handler.handle(null);
        }
    }

    private void checkPassword(JsonArray result, String password, String username, Handler<String> handler) {
        JsonObject r = result.getJsonObject(0);
        String dbPassword;
        if (r != null && (dbPassword = r.getString("password")) != null
                && !getOrElse(r.getBoolean("blockedProfile"), false)) {
            boolean success = false;
            String hash = null;
            try {
                switch (dbPassword.length()) {
                case 32: // md5
                    hash = Md5.hash(password);
                    break;
                case 64: // sha-256
                    hash = Sha256.hash(password);
                    break;
                default: // BCrypt
                    success = BCrypt.checkpw(password, dbPassword);
                }
                if (!success && hash != null) {
                    success = !dbPassword.trim().isEmpty() && dbPassword.equalsIgnoreCase(hash);
                    if (success) {
                        upgradeOldPassword(username, password);
                    }
                }
            } catch (NoSuchAlgorithmException e) {
                log.error(e.getMessage(), e);
            }
            if (success) {
                handler.handle(r.getString("userId"));
            } else {
                handler.handle(null);
            }
        } else {
            handler.handle(null);
        }
    }

    private void upgradeOldPassword(final String username, String password) {
        String query = "MATCH (u:User {login: {login}}) SET u.password = {password} "
                + "RETURN u.id as id, HEAD(u.profiles) as profile ";
        JsonObject params = new JsonObject().put("login", username).put("password",
                BCrypt.hashpw(password, BCrypt.gensalt()));
        neo.execute(query, params, new io.vertx.core.Handler<Message<JsonObject>>() {
            @Override
            public void handle(Message<JsonObject> event) {
                if (!"ok".equals(event.body().getString("status"))) {
                    log.error("Error updating old password for user " + username + " : "
                            + event.body().getString("message"));
                } else if (event.body().getJsonArray("result") != null
                        && event.body().getJsonArray("result").size() == 1) {
                    // welcome message
                    JsonObject message = new JsonObject()
                            .put("userId", event.body().getJsonArray("result").getJsonObject(0).getString("id"))
                            .put("profile",
                                    event.body().getJsonArray("result").getJsonObject(0).getString("profile"))
                            .put("request",
                                    new JsonObject().put("headers", new JsonObject()
                                            .put("Accept-Language", getRequest().getHeader("Accept-Language"))
                                            .put("Host", getRequest().getHeader("Host"))
                                            .put("X-Forwarded-Host", getRequest().getHeader("X-Forwarded-Host"))));
                    neo.getEventBus().publish("send.welcome.message", message);
                }
            }
        });
    }

    @Override
    public void createOrUpdateAuthInfo(String clientId, String userId, String scope, Handler<AuthInfo> handler) {
        createOrUpdateAuthInfo(clientId, userId, scope, null, handler);
    }

    public void createOrUpdateAuthInfo(final String clientId, final String userId, final String scope,
            final String redirectUri, final Handler<AuthInfo> handler) {
        if (clientId != null && userId != null && !clientId.trim().isEmpty() && !userId.trim().isEmpty()) {
            if (scope != null && !scope.trim().isEmpty()) {
                String query = "MATCH (app:`Application` {name:{clientId}}) RETURN app.scope as scope";
                neo.execute(query, new JsonObject().put("clientId", clientId),
                        new io.vertx.core.Handler<Message<JsonObject>>() {
                            @Override
                            public void handle(Message<JsonObject> res) {
                                JsonArray r = res.body().getJsonArray("result");

                                if ("ok".equals(res.body().getString("status")) && r != null && r.size() == 1) {
                                    JsonObject j = r.getJsonObject(0);
                                    if (j != null && j
                                            .getJsonArray("scope", new fr.wseduc.webutils.collections.JsonArray())
                                            .getList().containsAll(Arrays.asList(scope.split("\\s")))) {
                                        createAuthInfo(clientId, userId, scope, redirectUri, handler);
                                    } else {
                                        handler.handle(null);
                                    }
                                } else {
                                    handler.handle(null);
                                }
                            }
                        });
            } else {
                createAuthInfo(clientId, userId, scope, redirectUri, handler);
            }
        } else {
            handler.handle(null);
        }
    }

    private void createAuthInfo(String clientId, String userId, String scope, String redirectUri,
            final Handler<AuthInfo> handler) {
        final JsonObject auth = new JsonObject().put("clientId", clientId).put("userId", userId).put("scope", scope)
                .put("createdAt", MongoDb.now()).put("refreshToken", UUID.randomUUID().toString());
        if (redirectUri != null) {
            auth.put("redirectUri", redirectUri).put("code", UUID.randomUUID().toString());
        }
        mongo.save(AUTH_INFO_COLLECTION, auth, new io.vertx.core.Handler<Message<JsonObject>>() {

            @Override
            public void handle(Message<JsonObject> res) {
                if ("ok".equals(res.body().getString("status"))) {
                    auth.put("id", res.body().getString("_id"));
                    auth.remove("createdAt");
                    ObjectMapper mapper = new ObjectMapper();
                    try {
                        handler.handle(mapper.readValue(auth.encode(), AuthInfo.class));
                    } catch (IOException e) {
                        handler.handle(null);
                    }
                } else {
                    handler.handle(null);
                }
            }
        });
    }

    @Override
    public void createOrUpdateAccessToken(final AuthInfo authInfo, final Handler<AccessToken> handler) {
        if (authInfo != null) {
            final JsonObject query = new JsonObject().put("authId", authInfo.getId());
            mongo.count(ACCESS_TOKEN_COLLECTION, query, new io.vertx.core.Handler<Message<JsonObject>>() {
                @Override
                public void handle(Message<JsonObject> event) {
                    if ("ok".equals(event.body().getString("status")) && (event.body().getInteger("count", 1) == 0
                            || isNotEmpty(authInfo.getRefreshToken()))) {
                        final JsonObject token = new JsonObject().put("authId", authInfo.getId())
                                .put("token", UUID.randomUUID().toString()).put("createdOn", MongoDb.now())
                                .put("expiresIn", 3600);
                        if (openIdConnectService != null && authInfo.getScope() != null
                                && authInfo.getScope().contains("openid")) {
                            //"2.0".equals(RequestUtils.getAcceptVersion(getRequest().getHeader("Accept")))) {
                            openIdConnectService.generateIdToken(authInfo.getUserId(), authInfo.getClientId(),
                                    new io.vertx.core.Handler<AsyncResult<String>>() {
                                        @Override
                                        public void handle(AsyncResult<String> ar) {
                                            if (ar.succeeded()) {
                                                token.put("id_token", ar.result());
                                                persistToken(token);
                                            } else {
                                                log.error("Error generating id_token.", ar.cause());
                                                handler.handle(null);
                                            }
                                        }
                                    });
                        } else {
                            persistToken(token);
                        }
                    } else { // revoke existing token and code with same authId
                        mongo.delete(ACCESS_TOKEN_COLLECTION, query);
                        mongo.delete(AUTH_INFO_COLLECTION, new JsonObject().put("_id", authInfo.getId()));
                        handler.handle(null);
                    }
                }

                private void persistToken(final JsonObject token) {
                    mongo.save(ACCESS_TOKEN_COLLECTION, token, new io.vertx.core.Handler<Message<JsonObject>>() {

                        @Override
                        public void handle(Message<JsonObject> res) {
                            if ("ok".equals(res.body().getString("status"))) {
                                AccessToken t = new AccessToken();
                                t.setAuthId(authInfo.getId());
                                t.setToken(token.getString("token"));
                                t.setCreatedOn(new Date(token.getJsonObject("createdOn").getLong("$date")));
                                t.setExpiresIn(3600);
                                if (token.containsKey("id_token")) {
                                    t.setIdToken(token.getString("id_token"));
                                }
                                handler.handle(t);
                            } else {
                                handler.handle(null);
                            }
                        }
                    });
                }
            });
        } else {
            handler.handle(null);
        }
    }

    @Override
    public void getAuthInfoByCode(String code, final Handler<AuthInfo> handler) {
        if (code != null && !code.trim().isEmpty()) {
            JsonObject query = new JsonObject().put("code", code).put("createdAt", new JsonObject().put("$gte",
                    new JsonObject().put("$date", System.currentTimeMillis() - CODE_EXPIRES)));
            mongo.findOne(AUTH_INFO_COLLECTION, query, new io.vertx.core.Handler<Message<JsonObject>>() {

                @Override
                public void handle(Message<JsonObject> res) {
                    JsonObject r = res.body().getJsonObject("result");
                    if ("ok".equals(res.body().getString("status")) && r != null && r.size() > 0) {
                        r.put("id", r.getString("_id"));
                        r.remove("_id");
                        r.remove("createdAt");
                        ObjectMapper mapper = new ObjectMapper();
                        try {
                            handler.handle(mapper.readValue(r.encode(), AuthInfo.class));
                        } catch (IOException e) {
                            handler.handle(null);
                        }
                    } else {
                        handler.handle(null);
                    }
                }
            });
        } else {
            handler.handle(null);
        }
    }

    @Override
    public void getAuthInfoByRefreshToken(String refreshToken, final Handler<AuthInfo> handler) {
        if (refreshToken != null && !refreshToken.trim().isEmpty()) {
            JsonObject query = new JsonObject().put("refreshToken", refreshToken);
            mongo.findOne(AUTH_INFO_COLLECTION, query, new io.vertx.core.Handler<Message<JsonObject>>() {

                @Override
                public void handle(Message<JsonObject> res) {
                    if ("ok".equals(res.body().getString("status"))) {
                        JsonObject r = res.body().getJsonObject("result");
                        if (r == null) {
                            handler.handle(null);
                            return;
                        }
                        r.put("id", r.getString("_id"));
                        r.remove("_id");
                        r.remove("createdAt");
                        ObjectMapper mapper = new ObjectMapper();
                        try {
                            handler.handle(mapper.readValue(r.encode(), AuthInfo.class));
                        } catch (IOException e) {
                            handler.handle(null);
                        }
                    } else {
                        handler.handle(null);
                    }
                }
            });
        } else {
            handler.handle(null);
        }
    }

    @Override
    public void getClientUserId(String clientId, String clientSecret, Handler<String> handler) {
        handler.handle("OAuthSystemUser");
    }

    @Override
    public void validateClientById(String clientId, final Handler<Boolean> handler) {
        if (clientId != null && !clientId.trim().isEmpty()) {
            String query = "MATCH (n:Application) " + "WHERE n.name = {clientId} " + "RETURN count(n) as nb";
            Map<String, Object> params = new HashMap<>();
            params.put("clientId", clientId);
            neo.execute(query, params, new io.vertx.core.Handler<Message<JsonObject>>() {

                @Override
                public void handle(Message<JsonObject> res) {
                    JsonArray a = res.body().getJsonArray("result");
                    if ("ok".equals(res.body().getString("status")) && a != null && a.size() == 1) {
                        JsonObject r = a.getJsonObject(0);
                        handler.handle(r != null && r.getInteger("nb") == 1);
                    } else {
                        handler.handle(false);
                    }
                }
            });
        } else {
            handler.handle(false);
        }
    }

    @Override
    public void validateUserById(String userId, final Handler<Boolean> handler) {
        if (userId != null && !userId.trim().isEmpty()) {
            String query = "MATCH (n:User) " + "WHERE n.id = {userId} " + "RETURN count(n) as nb";
            Map<String, Object> params = new HashMap<>();
            params.put("userId", userId);
            neo.execute(query, params, new io.vertx.core.Handler<Message<JsonObject>>() {

                @Override
                public void handle(Message<JsonObject> res) {
                    JsonArray a = res.body().getJsonArray("result");
                    if ("ok".equals(res.body().getString("status")) && a != null && a.size() == 1) {
                        JsonObject r = a.getJsonObject(0);
                        handler.handle(r != null && r.getInteger("nb") == 1);
                    } else {
                        handler.handle(false);
                    }
                }
            });
        } else {
            handler.handle(false);
        }
    }

    @Override
    public void getAccessToken(String token, final Handler<AccessToken> handler) {
        if (token != null && !token.trim().isEmpty()) {
            JsonObject query = new JsonObject().put("token", token);
            mongo.findOne(ACCESS_TOKEN_COLLECTION, query, new io.vertx.core.Handler<Message<JsonObject>>() {

                @Override
                public void handle(Message<JsonObject> res) {
                    JsonObject r = res.body().getJsonObject("result");
                    if ("ok".equals(res.body().getString("status")) && r != null && r.size() > 0) {
                        AccessToken t = new AccessToken();
                        t.setAuthId(r.getString("authId"));
                        t.setToken(r.getString("token"));
                        t.setCreatedOn(MongoDb.parseIsoDate(r.getJsonObject("createdOn")));
                        t.setExpiresIn(r.getInteger("expiresIn"));
                        handler.handle(t);
                    } else {
                        handler.handle(null);
                    }
                }
            });
        } else {
            handler.handle(null);
        }
    }

    @Override
    public void getAuthInfoById(String id, final Handler<AuthInfo> handler) {
        if (id != null && !id.trim().isEmpty()) {
            JsonObject query = new JsonObject().put("_id", id);
            mongo.findOne(AUTH_INFO_COLLECTION, query, new io.vertx.core.Handler<Message<JsonObject>>() {

                @Override
                public void handle(Message<JsonObject> res) {
                    if ("ok".equals(res.body().getString("status"))) {
                        JsonObject r = res.body().getJsonObject("result");
                        r.put("id", r.getString("_id"));
                        r.remove("_id");
                        r.remove("createdAt");
                        ObjectMapper mapper = new ObjectMapper();
                        try {
                            handler.handle(mapper.readValue(r.encode(), AuthInfo.class));
                        } catch (IOException e) {
                            handler.handle(null);
                        }
                    } else {
                        handler.handle(null);
                    }
                }
            });
        } else {
            handler.handle(null);
        }
    }

    private void getUserIdByLoginAlias(String username, String password, Handler<String> handler) {
        String query = "MATCH (n:User) " + "WHERE n.loginAlias={loginAlias} AND NOT(n.password IS NULL) "
                + "AND (NOT(HAS(n.blocked)) OR n.blocked = false) ";

        query += "OPTIONAL MATCH (p:Profile) " + "WHERE HAS(n.profiles) AND p.name = head(n.profiles) "
                + "RETURN DISTINCT n.id as userId, n.password as password, p.blocked as blockedProfile";
        Map<String, Object> params = new HashMap<>();
        params.put("loginAlias", username);
        neo.execute(query, params, res -> {
            JsonArray result = res.body().getJsonArray("result");
            if ("ok".equals(res.body().getString("status")) && result != null && result.size() == 1) {
                checkPassword(result, password, username, handler);
            } else {
                handler.handle(null);
            }
        });
    }

}