package com.perforce.sso.server;
/*
Copyright (c) Perforce Software, Inc., 2011-2012. All rights reserved
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL PERFORCE
SOFTWARE, INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
User contributed content on the Perforce Public Depot is not supported by Perforce,
although it may be supported by its author. This applies to all contributions
even those submitted by Perforce employees.
*/
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.FileAppender;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.varia.NullAppender;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
/**
* The server-side SSO trigger. Responsible for
* accepting and validating the authentication ticket
* passed from the client.
*
* Messages are generally logged to a file, with the exception of
* error messages that the user needs to see. These are
* also written to the console.
* @author rdefauw
*
*/
public class KerbServer {
/**
* logger
*/
private static Logger logger = null;
/**
* keyword for no log file
*/
private static final String NO_LOG = "NONE";
/**
* keyword for appending to log file
*/
private static final String LOG_APPEND = "true";
/**
* Expected arguments:
* <ul>
* <li>log file name, set to <code>NONE</code> for no logging
* <li>append existing log: can be <code>true</code> or <code>false</code>
* <li>user name
* <li>Kerberos/AD realm
* <li>Kerberos/AD domain controller
* <li>Login configuration file
* <li>Service name
* </ul>
* @param args Command line arguments
*/
public static void main(String[] args) {
// check for log arguments
if(args.length < 2) {
System.out.println("Required logging argument missing");
System.exit(1);
} // no log args
try {
// start logger
String logFileName = args[0];
String appendLog = args[1];
if(logFileName.equals(NO_LOG)) {
BasicConfigurator.configure(new NullAppender());
} // no logging
else {
FileAppender fileAppender = new FileAppender(new PatternLayout(PatternLayout.DEFAULT_CONVERSION_PATTERN), logFileName, appendLog.equals(LOG_APPEND));
BasicConfigurator.configure(fileAppender);
} // use file log
logger = Logger.getLogger(KerbServer.class.getName());
// verify rest of command line
if(args.length != 7) {
logger.error("Invalid command line: got " + args.length + " arguments");
System.out.println("Invalid command line: got " + args.length + " arguments");
System.exit(1);
} // invalid command line
String uid = requiredArg(args[2], "User ID");
String realm = requiredArg(args[3], "Realm");
String domainController = requiredArg(args[4], "Domain controller");
String loginConfFile = requiredArg(args[5], "Login configuration file");
String serviceName = requiredArg(args[6], "Perforce service name");
// basic Kerberos properties
System.setProperty("java.security.krb5.realm", realm);
System.setProperty("java.security.krb5.kdc", domainController);
System.setProperty("java.security.auth.login.config", loginConfFile);
logger.debug("Set Kerberos properties");
// create a LoginContext based on the entry in the login.conf file
LoginContext lc = new LoginContext("ServicePrincipalLoginContext");
// login (effectively populating the Subject)
lc.login();
logger.info("Logged in as service principal");
// get the Subject that represents the service
Subject serviceSubject = lc.getSubject();
// read ticket from stdin
Base64 b64 = new Base64();
byte[] serviceTicket = b64.decode((new BufferedReader(new InputStreamReader(System.in))).readLine());
logger.debug("Read service ticket from file");
// decode ticket
ServiceTicketDecoder decoder = new ServiceTicketDecoder(serviceTicket, serviceName);
String clientName = (String) Subject.doAs(serviceSubject, decoder);
logger.info("Got service ticket for user " + clientName);
// make sure ticket is for right user
String clientNameBrief = clientName;
int index = clientName.indexOf("@");
if(index > 0) {
clientNameBrief = clientName.substring(0, index);
} // strip out domain name
if(false == uid.equals(clientNameBrief)) {
logger.error("Ticket is for " + clientNameBrief + ", but request was for " + uid);
System.out.println("Ticket is for " + clientNameBrief + ", but request was for " + uid);
System.exit(1);
}
System.exit(0);
} // try
catch(Exception e) {
if(null != logger) {
logger.error(e.getMessage(), e);
System.out.println(e.getMessage());
} // can use logger
else {
System.out.println("Unexpected error, probably while starting logging system: " + e.getMessage());
} // no logger
System.exit(1);
} // catch
} // main
/**
* verifies that a required argument is present
*/
private static String requiredArg(String input, String paramName) {
if(null == input || input.length() < 1) {
logger.error(paramName + " is required argument to this program");
System.out.println(paramName + " is required argument to this program");
System.exit(1);
} // no input
return input;
} // requiredArg
} // class
/**
* Responsible for validating the service ticket
* @author rdefauw
*
*/
final class ServiceTicketDecoder implements PrivilegedExceptionAction<String>
{
/**
* the service ticket
*/
protected byte[] serviceTicket;
/**
* service name
*/
private final String svcName;
/**
* logger
*/
private final Logger logger;
/**
* ctor: get service ticket and start logger
* @param serviceTicket
*/
public ServiceTicketDecoder(byte[] serviceTicket, String svcName)
{
// the run() method does not support any arguments, so we pass the service ticket in via the constructor
this.serviceTicket = serviceTicket;
this.svcName = svcName;
this.logger = Logger.getLogger(ServiceTicketDecoder.class.getName());
} // ctor
public String run() throws Exception
{
try
{
// GSSAPI is generic, but if you give it the following Object ID,
// it will decode Kerberos 5 service tickets
Oid kerberos5Oid = new Oid("1.2.840.113554.1.2.2");
// create a GSSManager, which will do the work
GSSManager gssManager = GSSManager.getInstance();
// tell the GSSManager the Kerberos name of the service (substitute your appropriate names here)
GSSName serviceName = gssManager.createName(this.svcName, GSSName.NT_USER_NAME);
// get the service's credentials. note that this run() method was called by Subject.doAs(),
// so the service's credentials (Service Principal Name and password) are already available in the Subject
GSSCredential serviceCredentials = gssManager.createCredential(serviceName, GSSCredential.INDEFINITE_LIFETIME, kerberos5Oid, GSSCredential.ACCEPT_ONLY);
this.logger.debug("Created service credentials");
// create a security context for decrypting the service ticket
GSSContext gssContext = gssManager.createContext(serviceCredentials);
// decrypt the service ticket
gssContext.acceptSecContext(this.serviceTicket, 0, this.serviceTicket.length);
this.logger.debug("Accepted service ticket");
// get the client name from the decrypted service ticket
// note that Active Directory created the service ticket, so we can trust it
String clientName = gssContext.getSrcName().toString();
this.logger.debug("got client name " + clientName + " from service ticket");
// clean up the context
gssContext.dispose();
// return the authenticated client name
return clientName;
} // try
catch (Exception ex)
{
throw new PrivilegedActionException(ex);
} // catch
} // run
} // class ServiceTicketDecoder