/** * $Revision $ * $Date $ * * Copyright (C) 2005-2010 Jive Software. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.openfire.plugin.ofmeet; import org.eclipse.jetty.security.DefaultIdentityService; import org.eclipse.jetty.security.IdentityService; import org.eclipse.jetty.security.LoginService; import org.eclipse.jetty.server.UserIdentity; import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.auth.AuthFactory; import org.jivesoftware.openfire.auth.AuthToken; import org.jivesoftware.openfire.auth.UnauthorizedException; import org.jivesoftware.openfire.plugin.spark.*; import org.jivesoftware.database.DbConnectionManager; import org.jivesoftware.openfire.user.*; import org.jivesoftware.util.*; import org.jivesoftware.openfire.muc.*; import org.jivesoftware.openfire.muc.spi.*; import org.jivesoftware.openfire.forms.spi.*; import org.jivesoftware.openfire.forms.*; import org.jivesoftware.openfire.group.*; import org.jivesoftware.openfire.event.GroupEventDispatcher; import org.dom4j.Element; import org.xmpp.packet.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.microsoft.aad.adal4j.AuthenticationContext; import com.microsoft.aad.adal4j.AuthenticationResult; import javax.security.auth.Subject; import java.io.Serializable; import java.security.Principal; import java.sql.*; import java.io.*; import java.util.*; import java.util.concurrent.*; /** * A Jetty login service that uses Openfire to authenticate users. */ public class OpenfireLoginService extends AbstractLifeCycle implements LoginService { private static final Logger Log = LoggerFactory.getLogger(OpenfireLoginService.class); private final static String AUTHORITY = "https://login.windows.net/common"; private final static String CLIENT_ID = "9ba1a5c7-f17a-4de9-a1f1-6178c8d51223"; public static final ConcurrentHashMap<String, AuthToken> authTokens = new ConcurrentHashMap<>(); public static final ConcurrentHashMap<String, UserIdentity> identities = new ConcurrentHashMap<>(); public static final ConcurrentHashMap<String, String> skypeids = new ConcurrentHashMap<>(); private IdentityService _identityService=new DefaultIdentityService(); private String _name; private UserManager userManager = XMPPServer.getInstance().getUserManager(); private String accessToken = null; private String refreshToken = null; private String idToken = null; private String givenName = null; private String familyName = null; protected OpenfireLoginService() { } public OpenfireLoginService(String name) { setName(name); } public String getName() { return _name; } public IdentityService getIdentityService() { return _identityService; } public void setIdentityService(IdentityService identityService) { if (isRunning()) throw new IllegalStateException("Running"); _identityService = identityService; } public void setName(String name) { if (isRunning()) throw new IllegalStateException("Running"); _name = name; } @Override protected void doStart() throws Exception { super.doStart(); } @Override protected void doStop() throws Exception { super.doStop(); } public void logout(UserIdentity identity) { Log.debug("logout {}",identity); identities.remove(identity.getUserPrincipal().getName()); } @Override public String toString() { return this.getClass().getSimpleName()+"["+_name+"]"; } public UserIdentity login(String username, Object credential) { String userName = username.toLowerCase(); String password = (String) credential; Log.debug( "login " + userName); // AuthFactory supports both a bare username, as well as user@domain. However, UserManager only accepts the bare // username. If the provided value includes a domain, use only the node-part (after verifying that it's actually // a user of our domain). // for other domains, authenticate by assuming it is a global identity like Azure AD final String[] parts = userName.split( "@", 2 ); if ( parts.length > 1 ) { if ( XMPPServer.getInstance().getServerInfo().getXMPPDomain().equals( parts[1] ) ) { userName = parts[0]; try { userManager.getUser(userName); if (!authenticateByOpenfire(userName, password)) { Log.error( "access denied, unknown username " + userName ); return null; } } catch (UserNotFoundException e) { Log.error( "user not found " + userName); return null; } } else { if (!authenticateByAzureAD(userName, password)) { Log.error( "access denied, unknown (user@domain) " + userName ); return null; } userName = (parts[0] + "_" + parts[1]).replaceAll("\\.", "_"); try { userManager.getUser(userName); } catch (UserNotFoundException e) { try { userManager.createUser(userName, accessToken, givenName + " " + familyName, parts[0] + "@" + parts[1]); } catch (Exception e1) { Log.error( "access denied, cannot create username (user.domain) " + userName, e1); return null; } } if (skypeids.containsKey(userName) == false) { boolean skypeAvailable = XMPPServer.getInstance().getPluginManager().getPlugin("ofskype") != null; if (skypeAvailable) { skypeids.put(userName, username); IQ iq = new IQ(IQ.Type.set); iq.setFrom(userName + "@" + XMPPServer.getInstance().getServerInfo().getXMPPDomain()); iq.setTo(XMPPServer.getInstance().getServerInfo().getXMPPDomain()); Element child = iq.setChildElement("request", "http://igniterealtime.org/protocol/ofskype"); child.setText("{'action':'start_skype_user', 'password':'" + password + "', 'sipuri':'" + username + "'}"); XMPPServer.getInstance().getIQRouter().route(iq); } } if (authTokens.containsKey(userName) == false) { updateDomainGroup(userName, parts[1], givenName + " " + familyName); AuthToken authToken = new AuthToken(userName); authTokens.put(userName, authToken); } } } else { try { userManager.getUser(userName); if (!authenticateByOpenfire(userName, password)) { Log.error( "access denied, unknown username " + userName ); return null; } } catch (UserNotFoundException e) { Log.error( "user not found " + userName); return null; } } UserIdentity identity = null; if (identities.containsKey(userName)) { identity = identities.get(userName); } else { Principal userPrincipal = new KnownUser(userName, credential); Subject subject = new Subject(); subject.getPrincipals().add(userPrincipal); subject.getPrivateCredentials().add(credential); subject.getPrincipals().add(new RolePrincipal("ofmeet")); subject.setReadOnly(); identity = _identityService.newUserIdentity(subject, userPrincipal, new String[] {"ofmeet"}); identities.put(userName, identity); } return identity; } public boolean validate(UserIdentity user) { Log.debug( "validate " + user); return user != null; } public boolean authenticateByOpenfire(String userName, String password) { try { AuthToken authToken = AuthFactory.authenticate( userName, password); authTokens.put(userName, authToken); return true; } catch ( UnauthorizedException e ) { Log.error( "access denied, bad password " + userName ); return false; } catch ( Exception e ) { Log.error( "access denied " + userName ); return false; } } private void updateDomainGroup(String username, String groupName, String nickname) { try { Group group = null; JID jid = XMPPServer.getInstance().createJID(username, null); try { group = GroupManager.getInstance().getGroup(groupName); } catch (GroupNotFoundException e) { ; group = GroupManager.getInstance().createGroup(groupName); group.getProperties().put("sharedRoster.showInRoster", "onlyGroup"); group.getProperties().put("sharedRoster.displayName", groupName); group.getProperties().put("sharedRoster.groupList", ""); } try { group.getMembers().remove(jid); } catch (Exception e) {} group.getMembers().add(jid); Map<String, Object> params = new HashMap<String, Object>(); params.put("member", jid.toString()); GroupEventDispatcher.dispatchEvent(group, GroupEventDispatcher.EventType.member_added, params); updateDomainRoom(groupName, "private", groupName); } catch(Exception e) { Log.error("updateDomainGroup exception ", e); } } private void updateDomainRoom(String roomName, String roomStatus, String description) { Log.debug( "createRoom " + roomName + " " + roomStatus); boolean isBookmarksAvailable = XMPPServer.getInstance().getPluginManager().getPlugin("bookmarks") != null; try { String domainName = JiveGlobals.getProperty("xmpp.domain", XMPPServer.getInstance().getServerInfo().getHostname()); if (XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatService("conference").hasChatRoom(roomName) == false) { MUCRoom room = XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatService("conference").getChatRoom(roomName); if (room == null) { room = XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatService("conference").getChatRoom(roomName, new JID("admin@"+domainName)); if (room != null) { configureRoom(room, description); if (isBookmarksAvailable) createBookMark(roomName, roomStatus, description); } } } } catch (Exception e) { Log.error("createRoom", e); } } private void createBookMark(String roomName, String roomStatus, String description) { Bookmark bookmark = GetBookmarkByName(roomName); List<String> groupCollection = new ArrayList<String>(); try { if (bookmark == null) { String roomJid = roomName.toLowerCase() + "@conference." + XMPPServer.getInstance().getServerInfo().getXMPPDomain(); bookmark = new Bookmark(Bookmark.Type.group_chat, roomName, roomJid); String id = "" + bookmark.getBookmarkID() + System.currentTimeMillis(); String rootUrlSecure = JiveGlobals.getProperty("ofmeet.root.url.secure", "https://" + XMPPServer.getInstance().getServerInfo().getHostname() + ":" + JiveGlobals.getProperty("httpbind.port.secure", "7443")); String url = rootUrlSecure + "/ofmeet/?b=" + id; bookmark.setProperty("url", url); bookmark.setProperty("description", description); bookmark.setProperty("autojoin", "true"); if (roomStatus.equals("public")) { bookmark.setGlobalBookmark(true); } else { groupCollection.add(roomName); bookmark.setGroups(groupCollection); } } } catch (Exception e) { Log.error("createBookMark", e); } } private void configureRoom(MUCRoom room, String description) { Log.debug( "configureRoom " + room.getID()); FormField field; XDataFormImpl dataForm = new XDataFormImpl(DataForm.TYPE_SUBMIT); field = new XFormFieldImpl("muc#roomconfig_roomdesc"); field.setType(FormField.TYPE_TEXT_SINGLE); field.addValue(description); dataForm.addField(field); field = new XFormFieldImpl("muc#roomconfig_roomname"); field.setType(FormField.TYPE_TEXT_SINGLE); field.addValue(room.getName()); dataForm.addField(field); field = new XFormFieldImpl("FORM_TYPE"); field.setType(FormField.TYPE_HIDDEN); field.addValue("http://jabber.org/protocol/muc#roomconfig"); dataForm.addField(field); field = new XFormFieldImpl("muc#roomconfig_changesubject"); field.addValue("1"); dataForm.addField(field); field = new XFormFieldImpl("muc#roomconfig_maxusers"); field.addValue("30"); dataForm.addField(field); field = new XFormFieldImpl("muc#roomconfig_presencebroadcast"); field.addValue("moderator"); field.addValue("participant"); field.addValue("visitor"); dataForm.addField(field); field = new XFormFieldImpl("muc#roomconfig_publicroom"); field.addValue("1"); dataForm.addField(field); field = new XFormFieldImpl("muc#roomconfig_persistentroom"); field.addValue("1"); dataForm.addField(field); field = new XFormFieldImpl("muc#roomconfig_moderatedroom"); field.addValue("0"); dataForm.addField(field); field = new XFormFieldImpl("muc#roomconfig_membersonly"); field.addValue("0"); dataForm.addField(field); field = new XFormFieldImpl("muc#roomconfig_allowinvites"); field.addValue("1"); dataForm.addField(field); field = new XFormFieldImpl("muc#roomconfig_passwordprotectedroom"); field.addValue("0"); dataForm.addField(field); field = new XFormFieldImpl("muc#roomconfig_whois"); field.addValue("moderator"); dataForm.addField(field); field = new XFormFieldImpl("muc#roomconfig_enablelogging"); field.addValue("1"); dataForm.addField(field); field = new XFormFieldImpl("x-muc#roomconfig_canchangenick"); field.addValue("1"); dataForm.addField(field); field = new XFormFieldImpl("x-muc#roomconfig_registration"); field.addValue("1"); dataForm.addField(field); // Keep the existing list of admins field = new XFormFieldImpl("muc#roomconfig_roomadmins"); for (JID jid : room.getAdmins()) { field.addValue(jid.toString()); } dataForm.addField(field); String domainName = JiveGlobals.getProperty("xmpp.domain", XMPPServer.getInstance().getServerInfo().getHostname()); field = new XFormFieldImpl("muc#roomconfig_roomowners"); field.addValue("admin@"+domainName); dataForm.addField(field); // Create an IQ packet and set the dataform as the main fragment IQ iq = new IQ(IQ.Type.set); Element element = iq.setChildElement("query", "http://jabber.org/protocol/muc#owner"); element.add(dataForm.asXMLElement()); try { // Send the IQ packet that will modify the room's configuration room.getIQOwnerHandler().handleIQ(iq, room.getRole()); } catch (Exception e) { Log.error("configureRoom exception ", e); } } private Bookmark GetBookmarkByName(String name) { Bookmark bookmark = null; Connection con = null; PreparedStatement pstmt = null; ResultSet rs = null; try { con = DbConnectionManager.getConnection(); pstmt = con.prepareStatement("SELECT bookmarkID from ofBookmark where bookmarkName=?"); pstmt.setString(1, name); rs = pstmt.executeQuery(); if (rs.next()) { long bookmarkID = rs.getLong(1); try { bookmark = new Bookmark(bookmarkID); } catch (Exception e) { } } } catch (SQLException e) { Log.error(e.getMessage(), e); } finally { DbConnectionManager.closeConnection(rs, pstmt, con); } return bookmark; } public boolean authenticateByAzureAD(String username, String password) { AuthenticationContext context = null; AuthenticationResult result = null; ExecutorService service = null; try { service = Executors.newFixedThreadPool(1); context = new AuthenticationContext(AUTHORITY, false, service); Future<AuthenticationResult> future = context.acquireToken("https://graph.windows.net", CLIENT_ID, username, password, null); result = future.get(); } catch (Exception e) { Log.error("of_authenticate_365", e); } finally { service.shutdown(); } if (result != null) { accessToken = result.getAccessToken(); refreshToken = result.getRefreshToken(); idToken = result.getIdToken(); givenName = result.getUserInfo().getGivenName(); familyName = result.getUserInfo().getFamilyName(); } return result != null; } public static class KnownUser implements UserPrincipal, Serializable { private static final long serialVersionUID = -6226920753748399662L; private final String _name; private final Object _credential; public KnownUser(String name, Object credential) { _name=name; _credential=credential; } public boolean authenticate(Object credentials) { return true; } public String getName() { return _name; } public boolean isAuthenticated() { return true; } @Override public String toString() { return _name; } } public interface UserPrincipal extends Principal,Serializable { boolean authenticate(Object credentials); public boolean isAuthenticated(); } public static class RolePrincipal implements Principal,Serializable { private static final long serialVersionUID = 2998397924051854402L; private final String _roleName; public RolePrincipal(String name) { _roleName=name; } public String getName() { return _roleName; } } }