/*
 * Copyright (c) 2003-2011
 * Distributed Systems Software.  All rights reserved.
 * See the file LICENSE for redistribution information.
 */

/*
 * local_cas_auth - CAS (Central Authentication Service) authentication module
 *
 * There are several modes of operation.
 *
 * Mode 1. DACS redirect to CAS interaction
 * a) A user hits a DACS-wrapped service for which access is denied because
 *    the user is not authenticated (by DACS).  The user is redirected to
 *    dacs_authenticate;
 * b) dacs_authenticate sees that CAS authentication has been configured and
 *    calls local_cas_auth (no ticket argument is present);
 * c) local_cas_auth asks dacs_authenticate to redirect the user to CAS;
 * d) CAS authenticates the user and redirects the user back to
 *    dacs_authenticate;
 * e) dacs_authenticate sees that CAS authentication has been configured
 *    and calls local_cas_auth (passing the session ticket argument);
 * f) local_cas_auth calls CAS, passing it the session ticket; if the reply
 *    from CAS indicates success, the user has authenticated, otherwise
 *    authentication failed.
 *
 * A variant of this mode is CAS Single Sign-On
 * This is as above, except that the user has already authenticated
 * through CAS and holds a CAS ticket granting cookie; CAS may not need to
 * re-prompt for a login.
 * a) as above;
 * b) as above;
 * c) as above, except browser also sends ticket granting cookie;
 * d) as above, except CAS may not need to prompt the user;
 * e) as above;
 * f) as above
 *
 * Mode 2. DACS calls CAS directly
 * DACS is given a username and password that it passes to CAS itself.
 * If CAS indicates successful authentication, DACS authentication succeeds.
 * The CAS ticket granting cookie is discarded.
 *
 * See:
 * http://www.ja-sig.org/products/cas/overview/protocol/index.html
 * http://www.yale.edu/tp/auth/cas20.html
 * http://www.ja-sig.org/products/cas/index.html
 */

#ifndef lint
static const char copyright[] =
"Copyright (c) 2003-2011\n\
Distributed Systems Software.  All rights reserved.";
static const char revid[] =
  "$Id: local_cas_auth.c 2528 2011-09-23 21:54:05Z brachman $";
#endif

#include "dacs.h"

static const char *log_module_name = "local_cas_auth";

int
local_cas_auth(char *username, char *password, char *aux, Cas_auth *cas_auth)
{
  int argnum, reply_len, status_code;
  char *auth_jurisdiction, *callback_url, *p, *cas_url, *cas_ticket;
  char *cookies[2], *r, *reply, *roles_reply;
  Dsvec *dsv, *response_headers;
  Http_params *params;
  Jurisdiction *jp;

  if (cas_auth == NULL || cas_auth->server_uri == NULL) {
	log_msg((LOG_ERROR_LEVEL, "CAS_SERVER_URI is not configured"));
	return(-1);
  }

  if (username != NULL && *username == '\0')
	username = NULL;
  if (password != NULL && *password == '\0')
	password = NULL;

  auth_jurisdiction = conf_val(CONF_JURISDICTION_NAME);
  if (get_jurisdiction_meta(auth_jurisdiction, &jp) == -1
	  || jp->dacs_url == NULL || *jp->dacs_url == '\0') {
	/*
	 * In the case of dacsauth, if a username and password are available
	 * but no config, we don't really care if the callback_url is "real"
	 * because it won't be invoked.
	 */
	if (username != NULL && password != NULL)
	  callback_url = ds_xprintf("https://%s.%s/bogus/dacs_authenticate",
								auth_jurisdiction,
								conf_val(CONF_FEDERATION_DOMAIN));
	else {
	  log_msg((LOG_ERROR_LEVEL, "Cannot get jurisdiction meta info for \"%s\"",
			   auth_jurisdiction));
	  return(-1);
	}
  }
  else {
	Ds ds;

	ds_init(&ds);
	ds_asprintf(&ds, "%s/dacs_authenticate", jp->dacs_url);
	ds_asprintf(&ds, "?DACS_JURISDICTION=%s",
				conf_val(CONF_JURISDICTION_NAME));
	if (cas_auth->redirect_args != NULL)
	  ds_asprintf(&ds, "&%s", cas_auth->redirect_args);
	callback_url = ds_buf(&ds);
  }
  log_msg((LOG_DEBUG_LEVEL, "cas_auth->server_uri=\"%s\"",
		   cas_auth->server_uri));
  log_msg((LOG_DEBUG_LEVEL, "callback_url=\"%s\"", callback_url));
  if ((cas_ticket = cas_auth->session_ticket) != NULL)
	log_msg((LOG_DEBUG_LEVEL, "Got CAS ticket=\"%s\"", cas_ticket));
  else
	log_msg((LOG_DEBUG_LEVEL, "No CAS ticket"));

  if (username != NULL || password != NULL) {
	char *s, *ticket;

	if (username == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "USERNAME argument must accompany PASSWORD"));
	  return(-1);
	}
	if (password == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "PASSWORD argument must accompany USERNAME"));
	  return(-1);
	}

	/* Get a CAS login page and find the embedded login ticket. */
	p = strsuffix(cas_auth->server_uri, strlen(cas_auth->server_uri), "/");
	/*
	cas_url = ds_xprintf("%s%slogin", cas_auth->server_uri,
	(p == NULL) ? "/" : "");
	*/
	cas_url = ds_xprintf("%s%slogin?service=%s",
						 cas_auth->server_uri, (p == NULL) ? "/" : "",
						 url_encode(callback_url, 0));
	log_msg((LOG_DEBUG_LEVEL,
			 "Authenticating via CAS with given USERNAME/PASSWORD"));
	log_msg((LOG_DEBUG_LEVEL, "cas_url=\"%s\"", cas_url));

	dsv = dsvec_init_size(NULL, sizeof(Http_params), 5);
	reply = NULL;
	reply_len = -1;
	cookies[0] = NULL;
	argnum = 0;
	if (http_invoke(cas_url, HTTP_GET_METHOD,
					ssl_verify ? HTTP_SSL_ON_VERIFY
					: (use_ssl ? HTTP_SSL_ON : HTTP_SSL_URL_SCHEME),
					argnum, (Http_params *) dsvec_base(dsv), NULL, cookies,
					&reply, &reply_len, &status_code, NULL) == -1
		|| status_code != 200) {
	  log_msg((LOG_ERROR_LEVEL,
			   "CAS login fetch failed, status_code=%d", status_code));
	  if (reply != NULL)
		log_msg((LOG_ERROR_LEVEL, "%s", reply));
	  return(-1);
	}
 
	/*
	 * The CAS reply isn't nice for us to deal with and we don't want to do
	 * a full HTML parse of it.  So we'll just do a hack and scan for the
	 * login ticket and extract it.
	 */
	if ((p = strstr(reply, "name=\"lt\"")) == NULL
		&& (p = strstr(reply, "name=lt")) == NULL) {
	no_ticket:
	  log_msg((LOG_ERROR_LEVEL, "Can't locate login ticket"));
	  log_msg((LOG_ERROR_LEVEL, "Got: \"%s\"", reply));
	  return(-1);
	}
	s = p;
	while (s > reply && *s != '<')
	  s--;
	if (*s != '<')
	  goto no_ticket;

	while (p < (reply + reply_len) && *p != '>')
	  p++;
	if (*p != '>')
	  goto no_ticket;
	*p = '\0';
	if ((p = strstr(s, "value=")) == NULL)
	  goto no_ticket;
	p += 6;
	if (*p == '\'' || *p == '"')
	  p++;
	ticket = p;
	while (*p != '\0' && (isalnum((int) *p) || *p == '-'))
	  p++;
	*p = '\0';
	log_msg((LOG_TRACE_LEVEL | LOG_SENSITIVE_FLAG,
			 "Located login ticket: %s", ticket));

	argnum = 0;
	params = http_param(dsv, "username", username, NULL, 0);
	argnum++;

	params = http_param(dsv, "password", password, NULL, 0);
	argnum++;

	params = http_param(dsv, "lt", ticket, NULL, 0);
	argnum++;

	/*
	 * Attempt CAS authentication of USERNAME with PASSWORD, passing the
	 * CAS login ticket.
	 * This is just as if the user did this manually.
	 * We don't care about what is returned (e.g., the CAS service ticket),
	 * only that authentication succeeded.
	 */
	reply = NULL;
	reply_len = -1;
	cookies[0] = NULL;
	response_headers = dsvec_init(NULL, sizeof(char *));
	if (http_invoke(cas_url, HTTP_POST_METHOD,
					ssl_verify ? HTTP_SSL_ON_VERIFY
					: (use_ssl ? HTTP_SSL_ON : HTTP_SSL_URL_SCHEME),
					argnum, (Http_params *) dsvec_base(dsv), NULL, cookies,
					&reply, &reply_len, &status_code,
					response_headers) == -1 || status_code != 200) {
	  log_msg((LOG_ERROR_LEVEL,
			   "CAS authentication failed, status_code=%d", status_code));
	  if (reply != NULL)
		log_msg((LOG_ERROR_LEVEL, "%s", reply));
	  return(-1);
	}
 
	if ((p = strstr(reply, callback_url)) == NULL) {
	no_service_ticket:
	  log_msg((LOG_ERROR_LEVEL, "Can't locate service ticket"));
	  return(-1);
	}

	p += strlen(callback_url);
	if (!strneq(p, "&ticket=", 8) && !strneq(p, "?ticket=", 8))
	  goto no_service_ticket;
	p += 8;
	
	cas_ticket = p;
	while (*p != '\0' && (isalnum((int) *p) || *p == '-'))
	  p++;
	*p = '\0';
	log_msg((LOG_TRACE_LEVEL | LOG_SENSITIVE_FLAG,
			 "Located service ticket: %s", cas_ticket));
	/*
	 * Fall through and validate the service ticket, as a double check.
	 */
  }

  if (cas_ticket == NULL) {
	/*
	 * There's no service ticket to validate and no USERNAME/PASSWORD,
	 * so the user must be redirected to CAS in this step.
	 */
	p = strsuffix(cas_auth->server_uri, strlen(cas_auth->server_uri), "/");
	cas_url = ds_xprintf("%s%slogin?service=%s",
						 cas_auth->server_uri, (p == NULL) ? "/" : "",
						 url_encode(callback_url, 0));
	log_msg((LOG_DEBUG_LEVEL, "Authenticating interactively via CAS"));
	log_msg((LOG_DEBUG_LEVEL, "cas_url=\"%s\"", cas_url));

	/*
	 * Have dacs_authenticate redirect its caller to cas_url.
	 * If CAS successfully authenticates the user, it will redirect the user
	 * back to dacs_authenticate which will invoke local_cas_auth again,
	 * but this time with a service ticket to validate.
	 */
	cas_auth->redirect_url = cas_url;

	return(-1);
  }

  /*
   * This is either a callback from CAS or a post-login check.
   * Validate the CAS service ticket.
   */
  p = strsuffix(cas_auth->server_uri, strlen(cas_auth->server_uri), "/");
  cas_url = ds_xprintf("%s%svalidate?service=%s&ticket=%s",
					   cas_auth->server_uri, (p == NULL) ? "/" : "",
					   url_encode(callback_url, 0),
					   url_encode(cas_ticket, 0));
  log_msg((LOG_DEBUG_LEVEL,
		   "Validating CAS service ticket using cas_url=\"%s\"", cas_url));

  dsv = dsvec_init_size(NULL, sizeof(Http_params), 5);
  argnum = 0;
  reply = NULL;
  reply_len = -1;
  cookies[0] = NULL;
  response_headers = dsvec_init(NULL, sizeof(char *));
  if (http_invoke(cas_url, HTTP_GET_METHOD,
				  ssl_verify ? HTTP_SSL_ON_VERIFY
				  : (use_ssl ? HTTP_SSL_ON : HTTP_SSL_URL_SCHEME),
				  argnum, (Http_params *) dsvec_base(dsv), NULL, cookies,
				  &reply, &reply_len, &status_code, response_headers) == -1
	  || status_code != 200) {
	log_msg((LOG_ERROR_LEVEL,
			 "CAS validation failed, status_code=%d", status_code));
	if (reply != NULL)
	  log_msg((LOG_ERROR_LEVEL, "%s", reply));
	return(-1);
  }
 
  /*
   * If authentication succeeded, the reply looks like:
   *    yes\n<username>\n[roles <role_string>\n]
   */
  log_msg((LOG_DEBUG_LEVEL, "CAS validation request succeeded"));
  roles_reply = NULL;
  if (username == NULL) {
	if (strprefix(reply, "yes\n") != NULL
		&& (p = strchr(reply + 4, (int) '\n')) != NULL) {
	  cas_auth->username = reply + 4;
	  *p = '\0';
	  roles_reply = p + 1;
	}
	else {
	  log_msg((LOG_ERROR_LEVEL, "Invalid reply from CAS validation: \"%s\"",
			   reply));
	  return(-1);
	}
  }
  else {
	if ((roles_reply =
		 strprefix(reply, ds_xprintf("yes\n%s\n", username))) == NULL) {
	  log_msg((LOG_ERROR_LEVEL,
			   "Invalid reply from CAS validation - username mismatch"));
	  return(-1);
	}
	cas_auth->username = strdup(username);
  }

  /* This is a DACS extension to supply roles. */
  if ((r = roles_reply) != NULL) {
	while (*r == ' ' || *r == '\t')
	  r++;
	if (*r != '\0' && (p = strchr(r, (int) '\n')) != NULL) {
	  *p = '\0';
	  if (is_valid_role_str(r)) {
		cas_auth->roles = r;
		log_msg((LOG_TRACE_LEVEL, "Got role string: \"%s\"", r));
	  }
	  else
		log_msg((LOG_ERROR_LEVEL, "Invalid role string, ignored: \"%s\"", r));
	}
  }

  log_msg((LOG_DEBUG_LEVEL, "CAS validation succeeded for \"%s\"",
		   cas_auth->username));
  return(0);
}

#ifdef PROG
int
main(int argc, char **argv)
{
  int emitted_dtd, i;
  char *errmsg, *jurisdiction, *username, *password, *aux;
  Auth_reply_ok ok;
  Cas_auth cas_auth;
  Kwv *kwv;

  errmsg = "Internal error";
  emitted_dtd = 0;
  username = password = aux = jurisdiction = NULL;
  cas_auth.session_ticket = cas_auth.redirect_url = NULL;
  cas_auth.username = cas_auth.server_uri = cas_auth.redirect_args = NULL;
  cas_auth.roles = NULL;

  if (dacs_init(DACS_LOCAL_SERVICE, &argc, &argv, &kwv, &errmsg) == -1) {
	/* If we fail here, we may not have a DTD with which to reply... */
  fail:
	if (password != NULL)
	  strzap(password);
	if (aux != NULL)
	  strzap(aux);
	if (emitted_dtd) {
	  printf("%s\n", make_xml_auth_reply_failed(NULL, cas_auth.redirect_url));
	  emit_xml_trailer(stdout);
	}
	if (errmsg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "Failed: reason=%s", errmsg));

	exit(1);
  }

  /* This must go after initialization. */
  emitted_dtd = emit_xml_header(stdout, "auth_reply");

  if (argc > 1) {
	errmsg = "Usage: unrecognized parameter";
	goto fail;
  }

  for (i = 0; i < kwv->nused; i++) {
	if (streq(kwv->pairs[i]->name, "USERNAME") && username == NULL)
	  username = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "PASSWORD") && password == NULL)
	  password = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "AUXILIARY") && aux == NULL)
	  aux = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "DACS_JURISDICTION")
			 && jurisdiction == NULL)
	  jurisdiction = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "CAS_TICKET")
			 && cas_auth.session_ticket == NULL)
	  cas_auth.session_ticket = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "CAS_REDIRECT_ARGS")
			 && cas_auth.redirect_args == NULL)
	  cas_auth.redirect_args = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "CAS_SERVER_URI")
			 && cas_auth.server_uri == NULL)
	  cas_auth.server_uri = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "DACS_VERSION"))
	  ;
	else
	  log_msg((LOG_TRACE_LEVEL, "Parameter: '%s'", kwv->pairs[i]->name));
  }

  /* Verify that we're truly responsible for DACS_JURISDICTION */
  if (dacs_verify_jurisdiction(jurisdiction) == -1) {
	errmsg = "Missing or incorrect DACS_JURISDICTION";
	goto fail;
  }

  if (cas_auth.server_uri == NULL) {
	errmsg = "Missing CAS_SERVER_URI argument";
	goto fail;
  }
  log_msg((LOG_DEBUG_LEVEL, "CAS_SERVER_URI=\"%s\"", cas_auth.server_uri));

  if (local_cas_auth(username, password, aux, &cas_auth) == -1) {
	errmsg = "Username/Password/Aux incorrect";
	goto fail;
  }

  if (password != NULL)
	strzap(password);
  if (aux != NULL)
	strzap(aux);

  ok.username = cas_auth.username;
  ok.roles_reply = NULL;
  /* If this wasn't specified, dacs_authenticate will use the default. */
  ok.lifetime = kwv_lookup_value(kwv, "CREDENTIALS_LIFETIME_SECS");

  if (cas_auth.roles != NULL) {
	ok.roles_reply = ALLOC(Roles_reply);
	ok.roles_reply->ok = ALLOC(Roles_reply_ok);
	ok.roles_reply->ok->roles = cas_auth.roles;
	ok.roles_reply->failed = NULL;
	ok.roles_reply->status = NULL;
	log_msg((LOG_TRACE_LEVEL, "Returning role string: \"%s\"",
			 cas_auth.roles));
  }

  printf("%s\n", make_xml_auth_reply_ok(&ok));

  emit_xml_trailer(stdout);
  exit(0);
}
#endif
