package com.perforce.sso.client;
/*
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.FileWriter;
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.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
import org.apache.log4j.*;
import org.apache.log4j.varia.NullAppender;
/**
* The client-side SSO trigger. Responsible for
* obtaining a Kerberos ticket-granting ticket.
*/
public class KerbClient {
/**
* 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>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(KerbClient.class.getName());
// verify rest of command line
if(args.length != 6) {
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 realm = requiredArg(args[2], "Realm");
String domainController = requiredArg(args[3], "Domain controller");
String loginConfFile = requiredArg(args[4], "Login configuration file");
String serviceName = requiredArg(args[5], "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("SignedOnUserLoginContext");
// login (effectively populating the Subject)
lc.login();
logger.info("Authenticated as current user");
// get the Subject that represents the signed-on user
Subject clientSubject = lc.getSubject();
// get service ticket
byte[] serviceTicket = (byte[]) Subject.doAs(clientSubject, new ServiceTicketGenerator(clientSubject, serviceName));
logger.info("Got encoded service ticket");
// encode ticket
Base64 b64 = new Base64();
String ticket = b64.encodeAsString(serviceTicket);
logger.debug("Service ticket: " + ticket);
// print to stdout
System.out.println(ticket);
logger.debug("Wrote ticket");
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 KerbClient
/**
* Responsible for obtaining the service ticket
*/
final class ServiceTicketGenerator implements PrivilegedExceptionAction< byte[] >
{
/**
* the current user
*/
private final Subject subject;
/**
* service name
*/
private final String svcName;
/**
* logger
*/
private final Logger logger;
/**
* ctor - start logger
*/
public ServiceTicketGenerator(Subject s, String svcName) {
this.subject = s;
this.svcName = svcName;
this.logger = Logger.getLogger(ServiceTicketGenerator.class.getName());
} // ctor
public byte[] run() throws Exception
{
try
{
// GSSAPI is generic, but if you give it the following Object ID,
// it will create 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 client and service (substitute your appropriate names here)
String theClientName = subject.getPrincipals().iterator().next().getName();
this.logger.info("Authenticating as: " + theClientName);
GSSName clientName = gssManager.createName(theClientName, GSSName.NT_USER_NAME);
GSSName serviceName = gssManager.createName(this.svcName, null);
// get the client's credentials. note that this run() method was called by Subject.doAs(),
// so the client's credentials (Kerberos TGT or Ticket-Granting Ticket) are already available in the Subject
GSSCredential clientCredentials = gssManager.createCredential(clientName, 8*60*60, kerberos5Oid, GSSCredential.INITIATE_ONLY);
this.logger.debug("Created client credentials");
// create a security context between the client and the service
GSSContext gssContext = gssManager.createContext(serviceName, kerberos5Oid, clientCredentials, GSSContext.DEFAULT_LIFETIME);
this.logger.debug("Created service context");
// initialize the security context
// this operation will cause a Kerberos request of Active Directory,
// to create a service ticket for the client to use the service
byte[] serviceTicket = gssContext.initSecContext(new byte[0], 0, 0);
logger.info("Created service ticket");
gssContext.dispose();
// return the Kerberos service ticket as an array of encrypted bytes
return serviceTicket;
} // try
catch (Exception ex)
{
throw new PrivilegedActionException(ex);
} // catch
} // run
} // class ServiceTicketGenerator