/* * Copyright (C) 2020 by Savoir-faire Linux * Authors: William Enright <william.enright@savoirfairelinux.com> * Ndeye Anna Ndiaye <anna.ndiaye@savoirfairelinux.com> * Johnny Flores <johnny.flores@savoirfairelinux.com> * Mohammed Raza <mohammed.raza@savoirfairelinux.com> * Felix Sidokhine <felix.sidokhine@savoirfairelinux.com> * * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package net.jami.jams.authmodule; import lombok.extern.slf4j.Slf4j; import net.jami.datastore.main.DataStore; import net.jami.jams.common.authentication.AuthenticationSource; import net.jami.jams.common.authentication.AuthenticationSourceType; import net.jami.jams.common.authmodule.AuthModuleKey; import net.jami.jams.common.authmodule.AuthScope; import net.jami.jams.common.authmodule.AuthTokenResponse; import net.jami.jams.common.authmodule.AuthenticationModule; import net.jami.jams.common.cryptoengineapi.CertificateAuthority; import net.jami.jams.common.dao.StatementElement; import net.jami.jams.common.dao.StatementList; import net.jami.jams.common.jami.NameServer; import net.jami.jams.common.objects.user.AccessLevel; import net.jami.jams.common.objects.user.User; import net.jami.jams.common.utils.LibraryLoader; import net.jami.jams.common.utils.X509Utils; import org.bouncycastle.cert.X509CRLHolder; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPublicKey; import java.util.concurrent.ConcurrentHashMap; @Slf4j public class UserAuthenticationModule implements AuthenticationModule { //This contains the DOMAIN-SOURCE. //In general there is at most 2 here. private final static String LDAP_CONNECTOR_CLASS = "net.jami.jams.ldap.connector.LDAPConnector"; private final static String AD_CONNECTOR_CLASS = "net.jami.jams.ad.connector.ADConnector"; public static DataStore datastore; public static CertificateAuthority certificateAuthority; private final TokenController tokenController; private PrivateKey privateKey = null; private PublicKey publicKey = null; private final ConcurrentHashMap<AuthModuleKey, AuthenticationSource> authenticationSources = new ConcurrentHashMap<>(); public UserAuthenticationModule(DataStore dataStore, CertificateAuthority certificateAuthority) throws Exception{ UserAuthenticationModule.datastore = dataStore; UserAuthenticationModule.certificateAuthority = certificateAuthority; authenticationSources.put(new AuthModuleKey("LOCAL",AuthenticationSourceType.LOCAL),datastore); log.info("Started authentication module - default local source is already enabled!"); File pubkeyFile = new File(System.getProperty("user.dir") + File.separator + "oauth.pub"); File privateKeyFile = new File(System.getProperty("user.dir") + File.separator + "oauth.key"); if(!privateKeyFile.exists() || !pubkeyFile.exists()){ log.info("Generating first time private/public keys for OAuth!"); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(4096); KeyPair kp = keyPairGenerator.generateKeyPair(); privateKey = kp.getPrivate(); publicKey = kp.getPublic(); //Store these to file. OutputStream os; os = new FileOutputStream(System.getProperty("user.dir") + File.separator + "oauth.key"); os.write(X509Utils.getPEMStringFromPrivateKey(privateKey).getBytes()); os.flush(); os.close(); log.info("Succesfully stored OAuth private key for future use..."); os = new FileOutputStream(System.getProperty("user.dir") + File.separator + "oauth.pub"); os.write(X509Utils.getPEMStringFromPubKey(publicKey).getBytes()); os.flush(); os.close(); log.info("Succesfully stored OAuth public key for future use..."); } else{ InputStream path; privateKey = X509Utils.getKeyFromPEMString(new String(new FileInputStream(privateKeyFile).readAllBytes())); log.info("Succesfully loaded OAuth private key!"); publicKey = X509Utils.getPubKeyFromPEMString(new String(new FileInputStream(pubkeyFile).readAllBytes())); log.info("Succesfully loaded OAuth public key!"); } //TODO: Read signing key, if file does not exist create it (also create the corresponding public key) tokenController = new TokenController(privateKey); //Also expose the public key programatically. log.info("OAuth2 Token System instantiated succesfully!"); } @Override public void attachAuthSource(AuthenticationSourceType type, String settings) { switch (type){ case AD: loadAuthConnector(AD_CONNECTOR_CLASS,settings); break; case LDAP: loadAuthConnector(LDAP_CONNECTOR_CLASS,settings); break; default: break; } } private void loadAuthConnector(String className, String settings){ try { Class<?> cls = LibraryLoader.classLoader.loadClass(className); AuthenticationSource source = (AuthenticationSource) cls.getConstructor(String.class).newInstance(settings); authenticationSources.put(new AuthModuleKey(source.getInfo().getRealm(),source.getInfo().getAuthenticationSourceType()), source); } catch (Exception e){ log.error("Could not load connector " + className + " with reason: " + e.toString()); } } @Override public AuthTokenResponse authenticateUser(String username, String password) { AuthTokenResponse res = null; if(datastore.userExists(username)){ StatementList statementList = new StatementList(); StatementElement statementElement = new StatementElement("username","=",username,""); statementList.addStatement(statementElement); User user = datastore.getUserDao().getObjects(statementList).get(0); if(authenticationSources.get(new AuthModuleKey(user.getRealm(),user.getUserType())) .authenticate(username,password)) return tokenController.getToken(user,null); } //The second case is much more violent, because we don't know in advance "where" this user comes //from, so we have to infer (this is only really true for "users", all others are usually pre-marked) //This is also the case when we store the user into the DAO - because he never existed before. for(AuthModuleKey key : authenticationSources.keySet()){ if(authenticationSources.get(key).authenticate(username,password)){ User user = new User(); user.setUsername(username); user.setAccessLevel(AccessLevel.USER); user.setRealm(key.getRealm()); user.setUserType(key.getType()); //This is legal with a null ONLY because in this case there is no relation with a external server. RegisterUserFlow.createUser(user,null); return tokenController.getToken(user,null); } } return res; } @Override public AuthTokenResponse authenticateUser(X509Certificate[] certificates, X509CRLHolder crl, X509Certificate ca) { //Extract the username for the certificate and verify that it is not revoked. X509Certificate clientCert = certificates[0]; try { //Check if the certificate is even valid. clientCert.checkValidity(); //Check if the certificate was provided by a valid authority. clientCert.verify(ca.getPublicKey()); //Here we need to make a request to the CRL to find out if it has been revoked. if(crl.getRevokedCertificate(clientCert.getSerialNumber()) != null) return null; //If the above cases have passed, then this user is indded valid. //This is yet to be confirmed. String username = clientCert.getSubjectDN().getName(); StatementList statementList = new StatementList(); StatementElement statementElement = new StatementElement("username","=",username,""); statementList.addStatement(statementElement); User user = datastore.getUserDao().getObjects(statementList).get(0); return tokenController.getToken(user, AuthScope.DEVICE); } catch (Exception e){ return null; } } @Override public ConcurrentHashMap<AuthModuleKey, AuthenticationSource> getAuthSources(){ return authenticationSources; } @Override public boolean testModuleConfiguration(AuthenticationSourceType type, String settings) { try { String className = ""; if (type.equals(AuthenticationSourceType.AD)) className = AD_CONNECTOR_CLASS; if (type.equals(AuthenticationSourceType.LDAP)) className = LDAP_CONNECTOR_CLASS; Class<?> cls = LibraryLoader.classLoader.loadClass(className); AuthenticationSource source = (AuthenticationSource) cls.getConstructor(String.class).newInstance(settings); return source.test(); } catch (Exception e){ log.error("The testing of the source was unsuccessful: " + e.toString()); return false; } } @Override public boolean createUser(AuthenticationSourceType type, String realm, NameServer nameServer, User user) { //This concept doesn't exist for LDAP or AD or any other hosted directory, in this case we simply run //very theoretically, we should allow LDAP to publish to the public registry, but this is a lot //more complex. return RegisterUserFlow.createUser(user,nameServer); } @Override public RSAPublicKey getAuthModulePubKey() { return (RSAPublicKey) publicKey; } @Override public char[] getOTP(String username) { if(datastore.userExists(username)){ StatementList statementList = new StatementList(); StatementElement statementElement = new StatementElement("username","=",username,""); statementList.addStatement(statementElement); User user = datastore.getUserDao().getObjects(statementList).get(0); return (user.getPassword()).toCharArray(); } return new char[0]; } }