implemented DIGEST authentication, which is for remote login more secure

as BASIC were pwd is transmitted near clear text (B64enc).
This has some implication as RFC 2617 requires and recommends a password hash MD5(user:realm:pwd) for DIGEST.

!!! before activating DIGEST you have to reassign all passwords !!! to allow new calculation of the hash
- default authentication is still BASIC
- configuration at this time only manually in (DATA/settings) or  defaults/web.xml  (<auth-method>
- the realmname is in defaults/yacy.init  adminRealm=YaCy-AdminUI
- fyi: the realmname is shown on login screen
- changing the realm name invalidates all passwords - but for security you are encouraged to do so (as localhostadmin)
- implemented to support both, old hashes for BASIC and new hashes for BASIC and DIGEST
- to differentiate old / new hash the in Jetty used hash-prefix "MD5:" is used for new pwd-hashes (  "MD5:hash" )
pull/1/head
reger 11 years ago
parent ba44eb1160
commit 0c754dd794

@ -68,18 +68,19 @@
<!-- security configuration --> <!-- security configuration -->
<!-- authentication method and default roles are preconfigured by the application (provided only for completness) --> <!-- authentication method and default roles are preconfigured by the application (provided only for completness) -->
<!--
<login-config> <login-config>
<!-- supported BASIC or DIGEST -->
<!-- Note: with DIGEST the name of this peer is part of password hashes
changing the peer name will invalidate all passwords
!!! Attention !!! prior to change this to DIGEST you have to reenter your password
to calculate a correct password hash -->
<auth-method>BASIC</auth-method> <auth-method>BASIC</auth-method>
<realm-name>YaCy Admin Interface</realm-name>
</login-config> </login-config>
-->
<!-- Roles --> <!-- Roles -->
<!--
<security-role> <security-role>
<role-name>adminRight</role-name> <role-name>adminRight</role-name>
<description>Administrator</description> <description>Administrator</description>
</security-role> </security-role>
-->
</web-app> </web-app>

@ -400,6 +400,16 @@ adminAccountUserName=admin
# inaccessibility for installations on headless servers. # inaccessibility for installations on headless servers.
adminAccountForLocalhost=true adminAccountForLocalhost=true
# adminRealm: a internal name (like a group name) for the login setting of the admin frontend
# ATTENTION: changing this name will invalidate all currently password hashes
# - With DIGEST authentication mode is this realm name of generated password hashes
# (RFC 2617 standard and recommendation). If you want to share password configuration
# with additional machines they have to belong to the same realm
# - authentication defaults to BASIC
# - and can be configured in defaults/web.xml , tag <auth-method>
adminRealm=YaCy-AdminUI
# if you are running a principal peer, you must update the following variables # if you are running a principal peer, you must update the following variables
# The upload method that should be used to upload the seed-list file to # The upload method that should be used to upload the seed-list file to
# a public accessible webserver where it can be loaded by other peers. # a public accessible webserver where it can be loaded by other peers.

@ -33,8 +33,6 @@ import java.util.EnumMap;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import net.yacy.cora.order.Base64Order;
import net.yacy.cora.order.Digest; import net.yacy.cora.order.Digest;
import net.yacy.cora.protocol.Domains; import net.yacy.cora.protocol.Domains;
import net.yacy.cora.protocol.RequestHeader; import net.yacy.cora.protocol.RequestHeader;
@ -65,7 +63,8 @@ public class ConfigAccounts_p {
// may be overwritten if new password is given // may be overwritten if new password is given
if (user.length() > 0 && pw1.length() > 3 && pw1.equals(pw2)) { if (user.length() > 0 && pw1.length() > 3 && pw1.equals(pw2)) {
// check passed. set account: // check passed. set account:
env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, Digest.encodeMD5Hex(Base64Order.standardCoder.encodeString(user + ":" + pw1))); // old: // env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, Digest.encodeMD5Hex(Base64Order.standardCoder.encodeString(user + ":" + pw1)));
env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, "MD5:"+Digest.encodeMD5Hex(user + ":" + sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy")+":"+ pw1));
env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT, ""); env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT, "");
env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME,user); env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME,user);
} else { } else {
@ -193,7 +192,9 @@ public class ConfigAccounts_p {
if( "newuser".equals(post.get("current_user"))){ //new user if( "newuser".equals(post.get("current_user"))){ //new user
if (!"".equals(pw1)) { //change only if set if (!"".equals(pw1)) { //change only if set
mem.put(UserDB.Entry.MD5ENCODED_USERPWD_STRING, Digest.encodeMD5Hex(username + ":" + pw1)); // MD5 according to HTTP Digest RFC 2617 (3.2.2) name:realm:pwd (use seed.hash as realm)
// with prefix of encoding method (supported MD5: )
mem.put(UserDB.Entry.MD5ENCODED_USERPWD_STRING, "MD5:"+Digest.encodeMD5Hex(username + ":" + sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy")+":"+pw1));
} }
mem.put(UserDB.Entry.USER_FIRSTNAME, firstName); mem.put(UserDB.Entry.USER_FIRSTNAME, firstName);
@ -222,7 +223,8 @@ public class ConfigAccounts_p {
if (entry != null) { if (entry != null) {
try{ try{
if (!"".equals(pw1)) { if (!"".equals(pw1)) {
entry.setProperty(UserDB.Entry.MD5ENCODED_USERPWD_STRING, Digest.encodeMD5Hex(username+":"+pw1)); // with prefix of encoding method (supported MD5: )
entry.setProperty(UserDB.Entry.MD5ENCODED_USERPWD_STRING, "MD5:"+Digest.encodeMD5Hex(username+ ":" + sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy") + ":"+pw1));
} }
entry.setProperty(UserDB.Entry.USER_FIRSTNAME, firstName); entry.setProperty(UserDB.Entry.USER_FIRSTNAME, firstName);

@ -92,7 +92,7 @@ public class SettingsAck_p {
return prop; return prop;
} }
// check passed. set account: // check passed. set account:
env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, Digest.encodeMD5Hex(Base64Order.standardCoder.encodeString(user + ":" + pw1))); env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, "MD5:"+Digest.encodeMD5Hex(user + ":" + sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy") + ":" + pw1));
env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT, ""); env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT, "");
env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, user); env.setConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, user);
prop.put("info", "5");//admin account changed prop.put("info", "5");//admin account changed

@ -125,28 +125,31 @@ public class User{
} }
} }
if(post!= null && entry != null){ if (post != null && entry != null) {
if(post.containsKey("changepass")){ if (post.containsKey("changepass")) {
prop.put("status", "1"); //password prop.put("status", "1"); //password
if(entry.getMD5EncodedUserPwd().equals(Digest.encodeMD5Hex(entry.getUserName()+":"+post.get("oldpass", "")))){
if(post.get("newpass").equals(post.get("newpass2"))){ if (entry.getMD5EncodedUserPwd().startsWith("MD5:") ?
if(!post.get("newpass", "").equals("")){ entry.getMD5EncodedUserPwd().equals("MD5:"+Digest.encodeMD5Hex(entry.getUserName() + ":" + sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy") + ":" + post.get("oldpass", ""))) :
try { entry.getMD5EncodedUserPwd().equals(Digest.encodeMD5Hex(entry.getUserName() + ":" + post.get("oldpass", "")))) {
entry.setProperty(UserDB.Entry.MD5ENCODED_USERPWD_STRING, Digest.encodeMD5Hex(entry.getUserName()+":"+post.get("newpass", ""))); if (post.get("newpass").equals(post.get("newpass2"))) {
prop.put("status_password", "0"); //changes if (!post.get("newpass", "").equals("")) {
} catch (final Exception e) { try {
ConcurrentLog.logException(e); entry.setProperty(UserDB.Entry.MD5ENCODED_USERPWD_STRING, "MD5:" + Digest.encodeMD5Hex(entry.getUserName() + ":" + sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy") + ":" + post.get("newpass", "")));
} prop.put("status_password", "0"); //changes
}else{ } catch (final Exception e) {
prop.put("status_password", "3"); //empty ConcurrentLog.logException(e);
} }
}else{ } else {
prop.put("status_password", "2"); //pws do not match prop.put("status_password", "3"); //empty
} }
}else{ } else {
prop.put("status_password", "1"); //old pw wrong prop.put("status_password", "2"); //pws do not match
} }
} } else {
prop.put("status_password", "1"); //old pw wrong
}
}
} }
if(post!=null && post.containsKey("logout")){ if(post!=null && post.containsKey("logout")){
prop.put("logged-in", "0"); prop.put("logged-in", "0");

@ -47,6 +47,8 @@ import net.yacy.cora.util.SpaceExceededException;
import net.yacy.kelondro.blob.MapHeap; import net.yacy.kelondro.blob.MapHeap;
import net.yacy.kelondro.util.FileUtils; import net.yacy.kelondro.util.FileUtils;
import net.yacy.kelondro.util.kelondroException; import net.yacy.kelondro.util.kelondroException;
import net.yacy.search.Switchboard;
import net.yacy.search.SwitchboardConstants;
public final class UserDB { public final class UserDB {
@ -126,16 +128,19 @@ public final class UserDB {
} }
/** /**
* Use a ProxyAuth String to authenticate user. * Use a ProxyAuth Value to authenticate user.
* @param auth base64 Encoded String, which contains "username:pw". * @param auth base64 Encoded String, which contains "username:pw".
*/ */
public Entry proxyAuth(final String auth) { public Entry proxyAuth(final String auth) {
Entry entry = null; Entry entry = null;
if (auth != null) { if (auth != null) {
final String[] tmp = Base64Order.standardCoder.decodeString(auth.trim().substring(6)).split(":"); final String[] tmp = Base64Order.standardCoder.decodeString(auth.trim()).split(":");
if (tmp.length == 2) { if (tmp.length == 2) {
entry = this.passwordAuth(tmp[0], tmp[1]); entry = this.passwordAuth(tmp[0], tmp[1]);
if (entry == null) {
entry = this.md5Auth(tmp[0], tmp[1]);
}
} }
} }
return entry; return entry;
@ -199,24 +204,26 @@ public final class UserDB {
} }
return null; return null;
} }
public Entry passwordAuth(final String user, final String password) { public Entry passwordAuth(final String user, final String password) {
final Entry entry = this.getEntry(user); final Entry entry = this.getEntry(user);
final String md5pwd; final String md5pwd;
if (entry != null && (md5pwd = entry.getMD5EncodedUserPwd()) != null && md5pwd.equals(Digest.encodeMD5Hex(user+":"+password))) { if (entry != null && (md5pwd = entry.getMD5EncodedUserPwd()) != null) {
if (entry.isLoggedOut()){
try { boolean authok;
entry.setProperty(Entry.LOGGED_OUT, "false"); if (md5pwd.startsWith("MD5:")) { // DIGEST style
} catch (final Exception e) { authok = md5pwd.equals("MD5:" + Digest.encodeMD5Hex(user + ":"
ConcurrentLog.logException(e); + Switchboard.getSwitchboard().getConfig(SwitchboardConstants.ADMIN_REALM, "YaCy") + ":" + password));
} } else {
authok = md5pwd.equals(Digest.encodeMD5Hex(user + ":" + password));
}
if (authok) {
return entry; return entry;
} }
return entry;
} }
return null; return null;
} }
public Entry passwordAuth(final String user, final String password, final String ip){ public Entry passwordAuth(final String user, final String password, final String ip){
final Entry entry = passwordAuth(user, password); final Entry entry = passwordAuth(user, password);
if (entry != null) { if (entry != null) {
@ -228,17 +235,9 @@ public final class UserDB {
public Entry md5Auth(final String user, final String md5) { public Entry md5Auth(final String user, final String md5) {
final Entry entry = this.getEntry(user); final Entry entry = this.getEntry(user);
if (entry != null && entry.getMD5EncodedUserPwd().equals(md5)) { if (entry != null && entry.getMD5EncodedUserPwd().endsWith(md5)) { // user pwd migth have prefix "MD5:"
if (entry.isLoggedOut()){
try {
entry.setProperty(Entry.LOGGED_OUT, "false");
} catch (final Exception e) {
ConcurrentLog.logException(e);
}
return null;
}
return entry; return entry;
} }
return null; return null;
} }
@ -328,8 +327,7 @@ public final class UserDB {
public class Entry { public class Entry {
public static final String MD5ENCODED_USERPWD_STRING = "MD5_user:pwd"; public static final String MD5ENCODED_USERPWD_STRING = "MD5_user:pwd";
public static final String AUTHENTICATION_METHOD = "auth_method";
public static final String LOGGED_OUT = "loggedOut";
public static final String USER_FIRSTNAME = "firstName"; public static final String USER_FIRSTNAME = "firstName";
public static final String USER_LASTNAME = "lastName"; public static final String USER_LASTNAME = "lastName";
public static final String USER_ADDRESS = "address"; public static final String USER_ADDRESS = "address";
@ -364,10 +362,6 @@ public final class UserDB {
this.mem = (mem == null) ? new HashMap<String, String>() : mem; this.mem = (mem == null) ? new HashMap<String, String>() : mem;
if (mem == null || !mem.containsKey(AUTHENTICATION_METHOD)) {
this.mem.put(AUTHENTICATION_METHOD,"yacy");
}
this.oldDate=Calendar.getInstance(); this.oldDate=Calendar.getInstance();
this.newDate=Calendar.getInstance(); this.newDate=Calendar.getInstance();
} }
@ -550,10 +544,6 @@ public final class UserDB {
return (this.mem.containsKey(accessRight.toString())) ? this.mem.get(accessRight.toString()).equals("true") : false; return (this.mem.containsKey(accessRight.toString())) ? this.mem.get(accessRight.toString()).equals("true") : false;
} }
public boolean isLoggedOut(){
return (this.mem.containsKey(LOGGED_OUT) ? this.mem.get(LOGGED_OUT).equals("true") : false);
}
public void logout(final String ip, final String logintoken){ public void logout(final String ip, final String logintoken){
logout(ip); logout(ip);
if(cookieUsers.containsKey(logintoken)){ if(cookieUsers.containsKey(logintoken)){
@ -563,7 +553,6 @@ public final class UserDB {
public void logout(final String ip) { public void logout(final String ip) {
try { try {
setProperty(LOGGED_OUT, "true");
if (ipUsers.containsKey(ip)){ if (ipUsers.containsKey(ip)){
ipUsers.remove(ip); ipUsers.remove(ip);
} }
@ -572,10 +561,6 @@ public final class UserDB {
} }
} }
public void logout(){
logout("xxxxxx");
}
@Override @Override
public String toString() { public String toString() {
final StringBuilder str = new StringBuilder(); final StringBuilder str = new StringBuilder();

@ -45,9 +45,8 @@ import net.yacy.http.servlets.SolrServlet;
import net.yacy.http.servlets.YaCyDefaultServlet; import net.yacy.http.servlets.YaCyDefaultServlet;
import net.yacy.http.servlets.YaCyProxyServlet; import net.yacy.http.servlets.YaCyProxyServlet;
import net.yacy.search.Switchboard; import net.yacy.search.Switchboard;
import net.yacy.search.SwitchboardConstants;
import net.yacy.utils.PKCS12Tool; import net.yacy.utils.PKCS12Tool;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
@ -160,14 +159,17 @@ public class Jetty8HttpServerImpl implements YaCyHttpServer {
allrequesthandlers.addHandler(htrootContext); allrequesthandlers.addHandler(htrootContext);
allrequesthandlers.addHandler(new DefaultHandler()); // if not handled by other handler allrequesthandlers.addHandler(new DefaultHandler()); // if not handled by other handler
// wrap all handlers by security handler YaCyLoginService loginService = new YaCyLoginService();
// this is very important (as it is part of the user password hash)
// changes will ivalidate all current existing user-password-hashes (from userDB)
loginService.setName(sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy"));
Jetty8YaCySecurityHandler securityHandler = new Jetty8YaCySecurityHandler(); Jetty8YaCySecurityHandler securityHandler = new Jetty8YaCySecurityHandler();
LoginService loginService = new YaCyLoginService();
securityHandler.setLoginService(loginService); securityHandler.setLoginService(loginService);
securityHandler.setRealmName(loginService.getName());
htrootContext.setSecurityHandler(securityHandler); htrootContext.setSecurityHandler(securityHandler);
// wrap all handlers
Handler crashHandler = new CrashProtectionHandler(allrequesthandlers); Handler crashHandler = new CrashProtectionHandler(allrequesthandlers);
// check server access restriction and add IPAccessHandler if restrictions are needed // check server access restriction and add IPAccessHandler if restrictions are needed
// otherwise don't (to save performance) // otherwise don't (to save performance)

@ -26,6 +26,8 @@ package net.yacy.http;
import net.yacy.cora.order.Base64Order; import net.yacy.cora.order.Base64Order;
import net.yacy.cora.order.Digest; import net.yacy.cora.order.Digest;
import net.yacy.search.Switchboard;
import net.yacy.search.SwitchboardConstants;
import net.yacy.server.serverAccessTracker; import net.yacy.server.serverAccessTracker;
import org.eclipse.jetty.util.security.Credential; import org.eclipse.jetty.util.security.Credential;
@ -34,27 +36,39 @@ import org.eclipse.jetty.util.security.Credential;
/** /**
* implementation of YaCy's old admin password as jetty Credential * implementation of YaCy's old admin password as jetty Credential
* supporting BASIC and DIGEST authentication
* and using MD5 encryptet passwords/credentials. Following RFC recommendation (to use the realm in MD5 hash)
* expecting a MD5 hash in format MD5( username:realm:password ), realm configured in yacy.init adminRealm
* (exception: old style credential MD5( username:password ) still accepted with BASIC auth)
*
*/ */
public class YaCyLegacyCredential extends Credential { public class YaCyLegacyCredential extends Credential {
private static final long serialVersionUID = -3527894085562480001L; private static final long serialVersionUID = -3527894085562480001L;
private String hash;
private String hash; // remember password hash (for new style with prefix of used encryption supported "MD5:" )
private String foruser; // remember the user as YaCy credential is username:pwd (not just pwd) private String foruser; // remember the user as YaCy credential is username:pwd (not just pwd)
private boolean isBase64enc; // remember hash encoding false = encodeMD5Hex(usr:pwd) ; true = encodeMD5Hex(Base64Order.standardCoder.encodeString(usr:pw)) private boolean isBase64enc; // remember hash encoding false = encodeMD5Hex(usr:pwd) ; true = encodeMD5Hex(Base64Order.standardCoder.encodeString(usr:pw))
private Credential c;
/** /**
* internal hash function * internal hash function for admin account
* *
* @param clear password * @param pw clear password
* @return hash string * @return hash string
*/ */
public static String calcHash(String pw) { public static String calcHash(String pw) { // old style hash
return Digest.encodeMD5Hex(Base64Order.standardCoder.encodeString(pw)); return Digest.encodeMD5Hex(Base64Order.standardCoder.encodeString(pw));
} }
@Override @Override
public boolean check(Object credentials) { public boolean check(Object credentials) {
if (credentials instanceof String) {
if (credentials instanceof Credential) { // for DIGEST auth
return ((Credential) credentials).check(c);
}
if (credentials instanceof String) { // for BASIC auth
final String pw = (String) credentials; final String pw = (String) credentials;
if (isBase64enc) { if (isBase64enc) {
if (serverAccessTracker.timeSinceAccessFromLocalhost() < 100) { if (serverAccessTracker.timeSinceAccessFromLocalhost() < 100) {
@ -63,42 +77,55 @@ public class YaCyLegacyCredential extends Credential {
// the cleartext password is not stored anywhere, but we must find a way to allow scripts to steer a peer. // the cleartext password is not stored anywhere, but we must find a way to allow scripts to steer a peer.
// this is the exception that makes that possible. // this is the exception that makes that possible.
// TODO: it should be better to check the actual access IP here, but that is not handed over to Credential classes :( // TODO: it should be better to check the actual access IP here, but that is not handed over to Credential classes :(
if (pw.equals(this.hash)) return true; if ((pw).equals(this.hash)) return true;
} }
// exception for admin use old style MD5hash (user:password)
return calcHash(foruser + ":" + pw).equals(this.hash); // for admin user return calcHash(foruser + ":" + pw).equals(this.hash); // for admin user
} }
// normal users
return Digest.encodeMD5Hex(foruser + ":" + pw).equals(this.hash); // normal users (and new admin pwd)
if (hash.startsWith(MD5.__TYPE) && hash != null) {
return (Digest.encodeMD5Hex(foruser + ":" + Switchboard.getSwitchboard().getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy")+":" + pw).equals(hash.substring(4)));
} else { // special check for old style password hash (prior v1.67/9501 15.1.2014)
return Digest.encodeMD5Hex(foruser + ":" + pw).equals(hash);
}
} }
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
/** /**
* create Credential object from config file hash * create Credential object from config file hash
* @param configHash hash as in config file hash(adminuser:pwd) *
* @return * @param configHash hash as in config file hash(adminuser:pwd)
*/ * @return
public static Credential getCredentialsFromConfig(String username, String configHash) { */
YaCyLegacyCredential c = new YaCyLegacyCredential(); public static Credential getCredentialForAdmin(String username, String configHash) {
c.foruser = username; YaCyLegacyCredential yc = new YaCyLegacyCredential();
c.isBase64enc = true; if (configHash.startsWith("MD5:")) {
c.hash = configHash; yc.isBase64enc = false;
return c; yc.c = Credential.getCredential(configHash);
} } else {
yc.isBase64enc = true;
/** }
* create Credential object from password yc.foruser = username;
* @param username yc.hash = configHash;
* @param configHash encodeMD5Hex("user:pwd") as stored in UserDB return yc;
* @return }
*/
public static Credential getCredentials(String username, String configHash) { /**
YaCyLegacyCredential c = new YaCyLegacyCredential(); * create Credential object from password
c.foruser = username; *
c.isBase64enc = false; * @param username
c.hash = configHash; * @param configHash encodeMD5Hex("user:realm:pwd") as stored in UserDB
//c.hash = calcHash(user + ":" + password); * @return
return c; */
} public static Credential getCredentialForUserDB(String username, String configHash) {
YaCyLegacyCredential yc = new YaCyLegacyCredential();
yc.c = Credential.getCredential(configHash); // creates a MD5 hash credential
yc.foruser = username;
yc.isBase64enc = false;
yc.hash = configHash;
return yc;
}
} }

@ -25,80 +25,68 @@
package net.yacy.http; package net.yacy.http;
import java.io.IOException; import java.io.IOException;
import java.security.Principal; import java.util.ArrayList;
import javax.security.auth.Subject;
import net.yacy.data.UserDB.AccessRight; import net.yacy.data.UserDB.AccessRight;
import net.yacy.data.UserDB.Entry; import net.yacy.data.UserDB.Entry;
import net.yacy.search.Switchboard; import net.yacy.search.Switchboard;
import net.yacy.search.SwitchboardConstants; import net.yacy.search.SwitchboardConstants;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService; import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.MappedLoginService; import org.eclipse.jetty.security.MappedLoginService;
import org.eclipse.jetty.server.UserIdentity; import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.util.security.Credential; import org.eclipse.jetty.util.security.Credential;
/** /**
* jetty login service, provides one admin user * jetty login service, provides admin and YaCy.UserDB users with role assignment
* with DIGEST auth by default Jetty uses the name of the loginSevice as realmname (which is part of all password hashes)
*/ */
public class YaCyLoginService extends MappedLoginService implements LoginService { public class YaCyLoginService extends MappedLoginService implements LoginService {
@Override
public String getName() {
return "YaCy 'admin' Account (reset your password with bin/passwd.sh <new password>)";
}
@Override @Override
protected UserIdentity loadUser(String username) { protected UserIdentity loadUser(String username) {
if (username == null || username.isEmpty()) return null; // quick exit
final Switchboard sb = Switchboard.getSwitchboard(); final Switchboard sb = Switchboard.getSwitchboard();
String adminuser = sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"); String adminuser = sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin");
Credential credential = null;
String[] roles = null;
if (username.equals(adminuser)) { if (username.equals(adminuser)) {
final String adminAccountBase64MD5 = sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, ""); final String adminAccountBase64MD5 = sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, "");
// in YaCy the credential hash is composed of username:pwd so the username is needed to create valid credential // in YaCy the credential hash is composed of username:pwd so the username is needed to create valid credential
// not just the password (as usually in Jetty). As the accountname for the std. adminuser is not stored a useridentity // not just the password (as usually in Jetty). As the accountname for the std. adminuser is not stored a useridentity
// is created for current user (and the pwd checked against the stored username:pwd setting) // is created for current user (and the pwd checked against the stored username:pwd setting)
Credential credential = YaCyLegacyCredential.getCredentialsFromConfig(username, adminAccountBase64MD5); credential = YaCyLegacyCredential.getCredentialForAdmin(username, adminAccountBase64MD5);
// TODO: YaCy user:pwd hashes should longterm likely be switched to separable username + pwd-hash entries // TODO: YaCy user:pwd hashes should longterm likely be switched to separable username + pwd-hash entries
// and/or the standard admin account username shuld be fix = "admin" // and/or the standard admin account username shuld be fix = "admin"
roles = new String[]{AccessRight.ADMIN_RIGHT.toString()};
Principal userPrincipal = new MappedLoginService.KnownUser(username, credential); } else {
Subject subject = new Subject(); Entry user = sb.userDB.getEntry(username);
subject.getPrincipals().add(userPrincipal); if (user != null && user.getMD5EncodedUserPwd() != null) {
subject.getPrivateCredentials().add(credential); // assigning roles from userDB
subject.setReadOnly(); ArrayList<String> roletmp = new ArrayList<String>();
IdentityService is = getIdentityService(); for (final AccessRight right : AccessRight.values()) {
return is.newUserIdentity(subject, userPrincipal, new String[]{AccessRight.ADMIN_RIGHT.toString()}); if (user.hasRight(right)) {
} roletmp.add(right.toString());
Entry user = sb.userDB.getEntry(username); }
if (user != null) {
// assigning roles from userDB
String[] role = new String[AccessRight.values().length];
int i = 0;
for (final AccessRight right : AccessRight.values()) {
if (user.hasRight(right)) {
role[i] = right.toString();
i++;
} }
if (roletmp.size() > 0) roles = roletmp.toArray(new String[roletmp.size()]);
credential = YaCyLegacyCredential.getCredentialForUserDB(username, user.getMD5EncodedUserPwd());
}
}
if (credential != null) {
if (roles != null) {
return putUser(username, credential, roles);
} else {
return putUser(username, credential); // w/o role makes not much sense, but succeeds login....
} }
Credential credential = YaCyLegacyCredential.getCredentials(username, user.getMD5EncodedUserPwd());
Principal userPrincipal = new MappedLoginService.KnownUser(username, credential);
Subject subject = new Subject();
subject.getPrincipals().add(userPrincipal);
subject.getPrivateCredentials().add(credential);
subject.setReadOnly();
IdentityService is = getIdentityService();
return is.newUserIdentity(subject, userPrincipal, role);
} }
return null; return null;
} }
@Override @Override
protected void loadUsers() throws IOException { protected void loadUsers() throws IOException {
// don't load any users into MappedLoginService on startup // don't load any users into MappedLoginService on startup
// we use loadUser for dynamic checking // we use loadUser for dynamic checking
} }
} }

@ -48,9 +48,14 @@ import javax.servlet.http.HttpServletResponse;
import net.yacy.cora.date.GenericFormatter; import net.yacy.cora.date.GenericFormatter;
import net.yacy.cora.document.analysis.Classification; import net.yacy.cora.document.analysis.Classification;
import net.yacy.cora.order.Base64Order;
import net.yacy.cora.protocol.Domains;
import net.yacy.cora.protocol.HeaderFramework; import net.yacy.cora.protocol.HeaderFramework;
import net.yacy.cora.protocol.RequestHeader; import net.yacy.cora.protocol.RequestHeader;
import net.yacy.cora.util.ConcurrentLog; import net.yacy.cora.util.ConcurrentLog;
import net.yacy.data.UserDB;
import net.yacy.data.UserDB.AccessRight;
import net.yacy.data.UserDB.Entry;
import net.yacy.http.ProxyHandler; import net.yacy.http.ProxyHandler;
import net.yacy.kelondro.util.FileUtils; import net.yacy.kelondro.util.FileUtils;
import net.yacy.kelondro.util.MemoryControl; import net.yacy.kelondro.util.MemoryControl;
@ -644,11 +649,28 @@ public class YaCyDefaultServlet extends HttpServlet {
legacyRequestHeader.put(HeaderFramework.CONNECTION_PROP_PATH, target); legacyRequestHeader.put(HeaderFramework.CONNECTION_PROP_PATH, target);
legacyRequestHeader.put(HeaderFramework.CONNECTION_PROP_EXT, targetExt); legacyRequestHeader.put(HeaderFramework.CONNECTION_PROP_EXT, targetExt);
// for userDB user legacyRequest expect login in Cookie (add one) if (legacyRequestHeader.containsKey(RequestHeader.AUTHORIZATION)) {
if (request.getUserPrincipal() != null) { if (HttpServletRequest.BASIC_AUTH.equalsIgnoreCase(request.getAuthType())) {
String userpassEncoded = request.getHeader("Authorization"); // e.g. "Basic xxXXxxXXxxXX" } else {
if (userpassEncoded != null) { // handle DIGEST auth for legacyHeader (create username:md5pwdhash
legacyRequestHeader.setCookie("login", userpassEncoded); if (request.getUserPrincipal() != null) {
String userpassEncoded = request.getHeader(RequestHeader.AUTHORIZATION); // e.g. "Basic AdminMD5hash"
if (userpassEncoded != null) {
if (request.isUserInRole(AccessRight.ADMIN_RIGHT.toString()) && !Switchboard.getSwitchboard().getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5,"").isEmpty()) {
// fake admin authentication for legacyRequestHeader (as e.g. DIGEST is not supported by legacyRequestHeader)
legacyRequestHeader.put(RequestHeader.AUTHORIZATION, HttpServletRequest.BASIC_AUTH + " "
+ Switchboard.getSwitchboard().getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, ""));
} else {
// fake Basic auth header for Digest auth (Basic username:md5pwdhash)
String username = request.getRemoteUser();
Entry user = Switchboard.getSwitchboard().userDB.getEntry(username);
if (user != null) {
legacyRequestHeader.put(RequestHeader.AUTHORIZATION, HttpServletRequest.BASIC_AUTH + " "
+ username + ":" + user.getMD5EncodedUserPwd());
}
}
}
}
} }
} }
return legacyRequestHeader; return legacyRequestHeader;
@ -842,7 +864,7 @@ public class YaCyDefaultServlet extends HttpServlet {
// handle action auth: check if the servlets requests authentication // handle action auth: check if the servlets requests authentication
if (templatePatterns.containsKey(serverObjects.ACTION_AUTHENTICATE)) { if (templatePatterns.containsKey(serverObjects.ACTION_AUTHENTICATE)) {
if (!request.authenticate(response)) { if (!request.authenticate(response)) {
return; return;
} }
//handle action forward //handle action forward
} else if (templatePatterns.containsKey(serverObjects.ACTION_LOCATION)) { } else if (templatePatterns.containsKey(serverObjects.ACTION_LOCATION)) {

@ -3233,6 +3233,8 @@ public final class Switchboard extends serverSwitch {
* http-authentify: auth-level 4 * http-authentify: auth-level 4
* *
* @param requestHeader * @param requestHeader
* - requestHeader..AUTHORIZATION = B64encode("adminname:password") or = B64encode("adminname:valueOf_Base64MD5cft")
* - adminAccountBase64MD5 = MD5(B64encode("adminname:password") or = "MD5:"+MD5("adminname:peername:password")
* @return the auth-level as described above or 1 which means 'not authorized'. a 0 is returned in case of * @return the auth-level as described above or 1 which means 'not authorized'. a 0 is returned in case of
* fraud attempts * fraud attempts
*/ */
@ -3254,8 +3256,13 @@ public final class Switchboard extends serverSwitch {
} }
// get the authorization string from the header // get the authorization string from the header
final String realmProp = (requestHeader.get(RequestHeader.AUTHORIZATION, "xxxxxx")).trim(); final String realmProp = (requestHeader.get(RequestHeader.AUTHORIZATION, "")).trim();
final String realmValue = realmProp.substring(6); String realmValue = realmProp.isEmpty() ? null : realmProp.substring(6); // take out "BASIC "
// authorization with admin keyword in configuration
if ( realmValue == null || realmValue.isEmpty() ) {
return 1;
}
// security check against too long authorization strings // security check against too long authorization strings
if ( realmValue.length() > 256 ) { if ( realmValue.length() > 256 ) {
@ -3264,24 +3271,42 @@ public final class Switchboard extends serverSwitch {
// authorization by encoded password, only for localhost access // authorization by encoded password, only for localhost access
String pass = Base64Order.standardCoder.encodeString(adminAccountUserName + ":" + adminAccountBase64MD5); String pass = Base64Order.standardCoder.encodeString(adminAccountUserName + ":" + adminAccountBase64MD5);
if ( accessFromLocalhost && (pass.equals(realmValue)) ) { if ( accessFromLocalhost && (pass.equals(realmValue)) ) { // assume realmValue as is in cfg
adminAuthenticationLastAccess = System.currentTimeMillis(); adminAuthenticationLastAccess = System.currentTimeMillis();
return 3; // soft-authenticated for localhost return 3; // soft-authenticated for localhost
} }
// authorization by hit in userDB // authorization by hit in userDB (realm username:encodedpassword - handed over by DefaultServlet)
if ( this.userDB.hasAdminRight(realmProp, requestHeader.getHeaderCookies()) ) { if ( this.userDB.hasAdminRight(realmValue, requestHeader.getHeaderCookies()) ) {
adminAuthenticationLastAccess = System.currentTimeMillis(); adminAuthenticationLastAccess = System.currentTimeMillis();
return 4; //return, because 4=max return 4; //return, because 4=max
} }
// authorization with admin keyword in configuration // athorization by BASIC auth (realmValue = "adminname:password")
if ( realmValue == null || realmValue.isEmpty() ) { if (adminAccountBase64MD5.startsWith("MD5:")) {
return 1; // handle new option adminAccountBase64MD5="MD5:xxxxxxx" = encodeMD5Hex ("adminname:peername:password")
String realmtmp = Base64Order.standardCoder.decodeString(realmValue); //decode to clear text
int i = realmtmp.indexOf(':');
if (i > 4) { // put peer name in realmValue (>4 is correct to scipt "MD5:" and usernames are min 4 characters)
realmtmp = realmtmp.substring(0, i + 1) + sb.getConfig(SwitchboardConstants.ADMIN_REALM,"YaCy") + ":" + realmtmp.substring(i + 1);
if (adminAccountBase64MD5.substring(4).equals(Digest.encodeMD5Hex(realmtmp))) {
adminAuthenticationLastAccess = System.currentTimeMillis();
return 4; // hard-authenticated, all ok
} }
if ( adminAccountBase64MD5.equals(Digest.encodeMD5Hex(realmValue)) ) { } else {
// handle DIGEST auth (realmValue = adminAccountBase (set for lecacyHeader in DefaultServlet for authenticated requests)
if (adminAccountBase64MD5.equals(realmValue)) {
adminAuthenticationLastAccess = System.currentTimeMillis(); adminAuthenticationLastAccess = System.currentTimeMillis();
return 4; // hard-authenticated, all ok return 4; // hard-authenticated, all ok
}
}
} else {
// handle old option adminAccountBase64MD5="xxxxxxx" = encodeMD55Hex(encodeB64("adminname:password")
if (adminAccountBase64MD5.equals(Digest.encodeMD5Hex(realmValue))) {
adminAuthenticationLastAccess = System.currentTimeMillis();
return 4; // hard-authenticated, all ok
}
} }
return 1; return 1;
} }

@ -40,11 +40,14 @@ public final class SwitchboardConstants {
* <p><code>public static final String <strong>ADMIN_ACCOUNT_B64MD5</strong> = "adminAccountBase64MD5"</code></p> * <p><code>public static final String <strong>ADMIN_ACCOUNT_B64MD5</strong> = "adminAccountBase64MD5"</code></p>
* <p>Name of the setting holding the authentication hash for the static <code>admin</code>-account. It is calculated * <p>Name of the setting holding the authentication hash for the static <code>admin</code>-account. It is calculated
* by first encoding <code>username:password</code> as Base64 and hashing it using {@link MapTools#encodeMD5Hex(String)}.</p> * by first encoding <code>username:password</code> as Base64 and hashing it using {@link MapTools#encodeMD5Hex(String)}.</p>
* With introduction of DIGEST authentication all passwords are MD5 encoded and calculatd as <code>username:adminrealm:password</code>
* To differentiate old and new admin passwords, use the new calculated passwords a "MD5:" prefix.
*/ */
public static final String ADMIN_ACCOUNT = "adminAccount"; public static final String ADMIN_ACCOUNT = "adminAccount";
public static final String ADMIN_ACCOUNT_B64MD5 = "adminAccountBase64MD5"; public static final String ADMIN_ACCOUNT_B64MD5 = "adminAccountBase64MD5";
public static final String ADMIN_ACCOUNT_USER_NAME = "adminAccountUserName"; // by default 'admin' public static final String ADMIN_ACCOUNT_USER_NAME = "adminAccountUserName"; // by default 'admin'
public static final String ADMIN_ACCOUNT_FOR_LOCALHOST = "adminAccountForLocalhost"; public static final String ADMIN_ACCOUNT_FOR_LOCALHOST = "adminAccountForLocalhost";
public static final String ADMIN_REALM = "adminRealm";
public static final int CRAWLJOB_SYNC = 0; public static final int CRAWLJOB_SYNC = 0;
public static final int CRAWLJOB_STATUS = 1; public static final int CRAWLJOB_STATUS = 1;

Loading…
Cancel
Save