/* zxidoauth.c  -  Handwritten nitty-gritty functions for constructing OAUTH URLs
 * Copyright (c) 2011 Sampo Kellomaki (sampo@iki.fi), All Rights Reserved.
 * This is confidential unpublished proprietary source code of the author.
 * NO WARRANTY, not even implied warranties. Contains trade secrets.
 * Distribution prohibited unless authorized in writing.
 * Licensed under Apache License 2.0, see file COPYING.
 * $Id$
 *
 * While this file contains some protocol encoders and decoders for OAUTH2,
 * the main logic of the flows is integrated to other parts, such as zxidsimp.c
 * 
 * http://openid.net/specs/openid-connect-basic-1_0.html
 * http://openid.net/specs/openid-connect-session-1_0.html
 * http://openid.net/specs/openid-connect-messages-1_0.html
 * http://tools.ietf.org/html/draft-ietf-oauth-v2-22
 * http://tools.ietf.org/html/draft-jones-json-web-encryption-01
 *
 * 11.12.2011, created --Sampo
 */

#include "platform.h"
#include "errmac.h"
#include "zx.h"
#include "zxid.h"
#include "zxidpriv.h"
#include "zxidutil.h"
#include "zxidconf.h"
#include "saml2.h"   /* for bindings like OAUTH2_REDIR */
#include "c/zx-data.h"

/*() Interpret ZXID standard form fields to construct a XML structure for AuthnRequest */

/* Called by:  zxid_start_sso_url */
struct zx_str* zxid_mk_oauth_az_req(zxid_conf* cf, zxid_cgi* cgi, struct zx_str* loc, char* relay_state)
{
  struct zx_str* ss;
  struct zx_str* nonce;
  struct zx_str* eid;
  char* eid_url_enc;
  char* redir_url_enc;
  char* state_b64;
  char* prompt;
  char* display;

  if (!loc) {
    ERR("Redirection location URL missing. %d", 0);
    return 0;
  }
  
  redir_url_enc = zx_url_encode(cf->ctx, strlen(cf->url), cf->url, 0);
  eid = zxid_my_ent_id(cf);
  eid_url_enc = zx_url_encode(cf->ctx, eid->len, eid->s, 0);
  zx_str_free(cf->ctx, eid);
  
  if (relay_state)
    state_b64 = zxid_deflate_safe_b64_raw(cf->ctx, strlen(relay_state), relay_state);
  else
    state_b64 = 0;
  nonce = zxid_mk_id(cf, "OA", ZXID_ID_BITS);
  prompt = BOOL_STR_TEST(cgi->force_authn) ? "login" : 0;
  prompt = BOOL_STR_TEST(cgi->consent && cgi->consent[0]) ? (prompt?"login+consent":"consent") : prompt;
  display = BOOL_STR_TEST(cgi->ispassive) ? "none" : 0;
  
  ss = zx_strf(cf->ctx,
	       "%.*s%cresponse_type=token+id_token"
	       "&client_id=%s"
	       "&scope=openid+profile+email+address"
	       "&redirect_uri=%s%%3fo=O"
	       "&nonce=%.*s"
	       "%s%s"           /* &state= */
	       "%s%s"           /* &display= */
	       "%s%s"           /* &prompt= */
	       CRLF2,
	       loc->len, loc->s, (memchr(loc->s, '?', loc->len)?'&':'?'),
	       eid_url_enc,
	       redir_url_enc,
	       nonce->len, nonce->s,
	       state_b64?"&state=":"", STRNULLCHK(state_b64),
	       display?"&display=":"", STRNULLCHK(display),
	       prompt?"&prompt=":"", STRNULLCHK(prompt)
	       );
  D("OAUTH2 AZ REQ(%.*s)", ss->len, ss->s);
  if (errmac_debug & ERRMAC_INOUT) INFO("%.*s", ss->len, ss->s);
  zx_str_free(cf->ctx, nonce);
  ZX_FREE(cf->ctx, state_b64);
  ZX_FREE(cf->ctx, eid_url_enc);
  ZX_FREE(cf->ctx, redir_url_enc);
  return ss;
}

/*() Construct OAUTH2 / OpenID-Connect1 id_token. */

/* Called by:  zxid_sso_issue_jwt */
char* zxid_mk_jwt(zxid_conf* cf, int claims_len, char* claims)
{
  char hash[64 /*EVP_MAX_MD_SIZE*/];
  char* jwt_hdr;
  int hdr_len;
  char* b64;
  char* p;
  int len = SIMPLE_BASE64_LEN(claims_len);
  
  switch (cf->oaz_jwt_sigenc_alg) {
  case 'n':
    jwt_hdr = "{\"typ\":\"JWT\",\"alg\":\"none\"}";
    hdr_len = strlen(jwt_hdr);
    len += SIMPLE_BASE64_LEN(hdr_len) + 1 + 1;    
    break;
  case 'h':
    jwt_hdr = "{\"typ\":\"JWT\",\"alg\":\"HS256\"}";
    hdr_len = strlen(jwt_hdr);
    len += SIMPLE_BASE64_LEN(hdr_len) + 1 + 1 + 86 /* siglen conservative estimate */;    
    break;
  case 'r':
    jwt_hdr = "{\"typ\":\"JWT\",\"alg\":\"RS256\"}";
    hdr_len = strlen(jwt_hdr);
    len += SIMPLE_BASE64_LEN(hdr_len) + 1 + 1 + 500 /* siglen conservative estimate */;    
    break;
  default:
    ERR("Unrecognized OAZ_JWT_SIGENC_ALG spec(%c). See zxid-conf.pd or zxidconf.h for documentation.", cf->oaz_jwt_sigenc_alg);
    return 0;
  }
  
  b64 = ZX_ALLOC(cf->ctx, len+1);
  p = base64_fancy_raw(jwt_hdr, hdr_len, b64, safe_basis_64, 1<<31, 0, 0, 0);
  *p++ = '.';
  p = base64_fancy_raw(claims, claims_len, p, safe_basis_64, 1<<31, 0, 0, 0);
  *p = 0;
  
  switch (cf->oaz_jwt_sigenc_alg) {
  case 'n':
    *p++ = '.';
    *p = 0;
    break;
  case 'h':
    if (!cf->hmac_key[0])
      zx_get_symkey(cf, "hmac.key", cf->hmac_key);
    zx_hmac_sha256(cf->ctx, ZX_SYMKEY_LEN, cf->hmac_key, p-b64, b64, hash, &len);
    *p++ = '.';
    p = base64_fancy_raw(hash, len, p, safe_basis_64, 1<<31, 0, 0, 0);
    *p = 0;
    break;
  case 'r':
    ERR("RSA not implemented yet %d",0);
    zx_hmac_sha256(cf->ctx, ZX_SYMKEY_LEN, cf->hmac_key, p-b64, b64, hash, &len);
    *p++ = '.';
    p = base64_fancy_raw(hash, len, p, safe_basis_64, 1<<31, 0, 0, 0);
    *p = 0;
    break;
  }
  D("JWT(%s)", b64);
  return b64;
}

/*() Issue OAUTH2 / OpenID-Connect1 id_token. */

/* Called by:  zxid_oauth2_az_server_sso */
char* zxid_sso_issue_jwt(zxid_conf* cf, zxid_cgi* cgi, zxid_ses* ses, struct timeval* srcts, zxid_entity* sp_meta, struct zx_str* acsurl, zxid_nid** nameid, char* logop)
{
  int rawlen;
  char* buf;
  char* jwt;
  char* jwt_id; /* sha1 hash of the jwt, taken from log_path */
  struct zx_str issuer;
  struct zx_str* affil;
  char* eid;
  struct zx_str* logpath;
  struct zx_str ss;
  struct zx_str nn;
  struct zx_str id;
  zxid_nid* tmpnameid;
  char sp_name_buf[ZXID_MAX_SP_NAME_BUF];
  D("sp_eid(%s)", sp_meta->eid);
  if (!nameid)
    nameid = &tmpnameid;

  //if (ar && ar->IssueInstant && ar->IssueInstant->g.len && ar->IssueInstant->g.s)
  //  srcts->tv_sec = zx_date_time_to_secs(ar->IssueInstant->g.s);
  
  if (!cgi->allow_create)
    cgi->allow_create = '1';
  if (!cgi->nid_fmt || !cgi->nid_fmt[0])
    cgi->nid_fmt = "prstnt";  /* Persistent is the default implied by the specs. */

  /* Check for federation. */
  
  issuer.s = cgi->client_id; issuer.len = strlen(cgi->client_id);
  affil = &issuer;
  zxid_nice_sha1(cf, sp_name_buf, sizeof(sp_name_buf), affil, affil, 7);
  D("sp_name_buf(%s)  allow_create=%d", sp_name_buf, cgi->allow_create);

  *nameid = zxid_get_fed_nameid(cf, &issuer, affil, ses->uid, sp_name_buf, cgi->allow_create,
				(cgi->nid_fmt && !strcmp(cgi->nid_fmt, "trnsnt")),
				srcts, 0, logop);
  if (logop) { logop[3]='S';  logop[4]='S';  logop[5]='O';  logop[6]=0;  /* Patch in SSO */ }
  if (!*nameid) {
    ERR("get_fed_nameid() client_id(%s) returned NULL", cgi->client_id);
    return 0;
  }

  eid = zxid_my_ent_id_cstr(cf);
  // ,\"\":\"\"
  buf = zx_alloc_sprintf(cf->ctx, &rawlen,
		       "{\"iss\":\"%s\""
		       ",\"user_id\":\"%.*s\""
		       ",\"aud\":\"%s\""
		       ",\"exp\":%d"
		       ",\"nonce\":\"%s\"}",
		       eid,
		       ZX_GET_CONTENT_LEN(*nameid), ZX_GET_CONTENT_S(*nameid),
		       cgi->client_id,
		       time(0) + cf->timeskew + cf->a7nttl,
		       cgi->nonce);
  ZX_FREE(cf->ctx, eid);
  jwt = zxid_mk_jwt(cf, rawlen, buf);
  ZX_FREE(cf->ctx, buf);

  /* Log the issued JWT */

  ss.s = jwt; ss.len = strlen(jwt);
  logpath = zxlog_path(cf, &issuer, &ss, ZXLOG_ISSUE_DIR, ZXLOG_JWT_KIND, 1);
  if (!logpath) {
    ERR("Could not generate logpath for aud(%s) JWT(%s)", cgi->client_id, jwt);
    ZX_FREE(cf->ctx, jwt);
    return 0;
  }
  
  /* Since JWT does not have explicit ID attribute, we use the sha1 hash of the
   * contents of JWT as an ID. Since this is what logpath also does, we just
   * use the last component of the logpath. */
  for (jwt_id = logpath->s + logpath->len; jwt_id > logpath->s && jwt_id[-1] != '/'; --jwt_id) ;

  if (cf->log_issue_a7n) {
    if (zxlog_dup_check(cf, logpath, "sso_issue_jwt")) {
      ERR("Duplicate JWT ID(%s)", jwt_id);
      if (cf->dup_a7n_fatal) {
	ERR("FATAL (by configuration): Duplicate JWT ID(%s)", jwt_id);
	zxlog_blob(cf, 1, logpath, &ss, "sso_issue_JWT dup");
	zx_str_free(cf->ctx, logpath);
	ZX_FREE(cf->ctx, jwt);
	return 0;
      }
    }
    zxlog_blob(cf, 1, logpath, &ss, "sso_issue_JWT");
  }

  nn.s = cgi->nonce; nn.len = strlen(cgi->nonce);
  id.s = jwt_id; id.len = strlen(jwt_id);

  if (cf->loguser)
    zxlogusr(cf, ses->uid, 0, 0, 0, &issuer, &nn, &id,
	     ZX_GET_CONTENT(*nameid),
	     (cf->oaz_jwt_sigenc_alg!='n'?"U":"N"), "K", logop, 0, 0);
  
  zxlog(cf, 0, 0, 0, &issuer, &nn, &id,
	ZX_GET_CONTENT(*nameid),
	(cf->oaz_jwt_sigenc_alg!='n'?"U":"N"), "K", logop, 0, 0);

  zx_str_free(cf->ctx, logpath);
  return jwt;
}

/*(i) Generate SSO assertion and ship it to SP by OAUTH2 Az redir binding. User has already
 * logged in by the time this is called. See also zxid_ssos_anreq() and zxid_idp_sso(). */

/* Called by:  zxid_idp_dispatch */
struct zx_str* zxid_oauth2_az_server_sso(zxid_conf* cf, zxid_cgi* cgi, zxid_ses* ses)
{
  zxid_entity* sp_meta;
  struct zx_str* acsurl = 0;
  struct timeval srcts = {0,501000};
  zxid_nid* nameid;
  char* idtok;
  char logop[8];
  strcpy(logop, "OAZxxxx");

  if (!cgi->client_id || !cgi->redirect_uri || !cgi->nonce) {
    ERR("Missing mandatory OAUTH2 field client_id=%p redirect_uri=%p nonce=%p", cgi->client_id, cgi->redirect_uri, cgi->nonce);
    return zx_dup_str(cf->ctx, "* ERR");
  }

  if (!cgi->response_type || !strstr(cgi->response_type, "token") || !strstr(cgi->response_type, "id_token")) {
    ERR("Missing mandatory OAUTH2 field response_type(%s) missing or does not contain `token id_token'", STRNULLCHKD(cgi->response_type));
    return zx_dup_str(cf->ctx, "* ERR");
  }

  if (!cgi->scope || !strstr(cgi->scope, "openid")) {
    ERR("Missing mandatory OAUTH2 field scope=%p or the scope does not contain `openid'", STRNULLCHKD(cgi->scope));
    return zx_dup_str(cf->ctx, "* ERR");
  }

  sp_meta = zxid_get_ent(cf, cgi->client_id);
  if (!sp_meta) {
    ERR("The metadata for client_id(%s) of the Az Req could not be found or fetched", cgi->client_id);
    return zx_dup_str(cf->ctx, "* ERR");
  }
  D("sp_eid(%s)", sp_meta->eid);

  /* Figure out the binding and url */

  acsurl = zxid_sp_loc_raw(cf, cgi, sp_meta, ZXID_ACS_SVC, OAUTH2_REDIR, 0);
  if (!acsurl) {
    ERR("sp(%s) metadata does not have SPSSODescriptor/AssertionConsumerService with Binding=\"" OAUTH2_REDIR "\". Pre-registering the SP at IdP is mandatory. redirect_uri(%s) will be ignored. (remote SP metadata problem)", sp_meta->eid, cgi->redirect_uri);
    return zx_dup_str(cf->ctx, "* ERR");
  }
  if (strlen(cgi->redirect_uri) != acsurl->len || memcmp(cgi->redirect_uri, acsurl->s, acsurl->len)) {
    ERR("sp(%s) metadata has SPSSODescriptor/AssertionConsumerService with Binding=\"" OAUTH2_REDIR "\" has value(%.*s), which is different from redirect_uri(%s). (remote SP problem)", sp_meta->eid, acsurl->len, acsurl->s, cgi->redirect_uri);
    return zx_dup_str(cf->ctx, "* ERR");
  }

  if (!cf->log_issue_a7n) {
    INFO("LOG_ISSUE_A7N must be turned on in IdP configuration for artifact profile to work. Turning on now automatically. %d", 0);
    cf->log_issue_a7n = 1;
  }

  /* User ses->uid is already logged in, now check for federation with sp */

  idtok = zxid_sso_issue_jwt(cf, cgi, ses, &srcts, sp_meta, acsurl, &nameid, logop);
  if (!idtok) {
    ERR("Issuing JWT Failed %s", logop);
    return zx_dup_str(cf->ctx, "* ERR");
  }
  
  D("OAUTH2-ART ep(%.*s)", acsurl->len, acsurl->s);
  zxlog(cf, 0, &srcts, 0, sp_meta->ed?&sp_meta->ed->entityID->g:0, 0, 0, ZX_GET_CONTENT(nameid), "N", "K", logop, ses->uid, "OAUTH2-ART");

  /* Formulate OAUTH2 / OpenID-Connect1 Az Redir Response */
  
  return zx_strf(cf->ctx, "Location: %s%c"
		 "access_token=%s"
		 "&token_type=bearer"
		 "&id_token=%s"
		 "&expires_in=%d" CRLF
		 "%s%s%s",   /* Set-Cookie */
		 cgi->redirect_uri, (strchr(cgi->redirect_uri, '?') ? '&' : '?'),
		 idtok,
		 idtok,
		 cf->a7nttl,
		 ses->setcookie?"Set-Cookie: ":"", ses->setcookie?ses->setcookie:"", ses->setcookie?CRLF:"");
}

/*() Extract an assertion from OAUTH Az response, and perform SSO */

/* Called by:  zxid_sp_oauth2_dispatch */
static int zxid_sp_dig_oauth_sso_a7n(zxid_conf* cf, zxid_cgi* cgi, zxid_ses* ses)
{
  //if (!zxid_chk_sig(cf, cgi, ses, &resp->gg, resp->Signature, resp->Issuer, 0, "Response")) return 0;
  
  //p = zxid_http_get(cf, url, &lim);

  ERR("*** process JWT %d", 0);

  //a7n = zxid_dec_a7n(cf, resp->Assertion, resp->EncryptedAssertion);
  //if (a7n) {
  //  zx_see_elem_ns(cf->ctx, &pop_seen, &resp->gg);
  //  return zxid_sp_sso_finalize(cf, cgi, ses, a7n, pop_seen);
  //}
  if (cf->anon_ok && cgi->rs && !strcmp(cf->anon_ok, cgi->rs))  /* Prefix match */
    return zxid_sp_anon_finalize(cf, cgi, ses);
  ERR("No Assertion found and not anon_ok in OAUTH Response %d", 0);
  zxlog(cf, 0, 0, 0, 0, 0, 0, ZX_GET_CONTENT(ses->nameid), "N", "C", "ERR", 0, "sid(%s) No assertion", ses->sid?ses->sid:"");
  return 0;
}

/*() Dispatch, on RP/SP side, OAUTH redir or artifact binding requests.
 *
 * return:: a string (such as Location: header) and let the caller output it.
 *     Sometimes a dummy string is just output to indicate status, e.g.
 *     "O" for SSO OK, "K" for normal OK no further action needed,
 *     "M" show management screen, "I" forward to IdP dispatch, or
 *     "* ERR" for error situations. These special strings
 *     are allocated from static storage and MUST NOT be freed. Other
 *     strings such as "Location: ..." should be freed by caller. */

/* Called by:  zxid_simple_no_ses_cf */
struct zx_str* zxid_sp_oauth2_dispatch(zxid_conf* cf, zxid_cgi* cgi, zxid_ses* ses)
{
  int ret;

  if (cgi->id_token) {  /* OAUTH2 artifact / redir biding, aka OpenID-Connect1 */    
    ret = zxid_sp_dig_oauth_sso_a7n(cf, cgi, ses);
    D("ret=%d ses=%p", ret, ses);
    switch (ret) {
    case ZXID_OK:      return zx_dup_str(cf->ctx, "K");
    case ZXID_SSO_OK:  return zx_dup_str(cf->ctx, "O");
    case ZXID_IDP_REQ: /* (PXY) Middle IdP of IdP Proxy flow */
      return zx_dup_str(cf->ctx, zxid_simple_ses_active_cf(cf, cgi, ses, 0, 0x1fff));
    case ZXID_FAIL:
      D("*** FAIL, should send back to IdP select %d", 0);
      return zx_dup_str(cf->ctx, "* ERR");
    }
    return zx_dup_str(cf->ctx, "M");  /* Management screen, please. */
  }
    
  if (cf->log_level > 0)
    zxlog(cf, 0, 0, 0, 0, 0, 0, ZX_GET_CONTENT(ses->nameid), "N", "C", "SPOADISP", 0, "sid(%s) unknown req or resp", STRNULLCHK(ses->sid));
  ERR("Unknown request or response %d", 0);
  return zx_dup_str(cf->ctx, "* ERR");
}

/*() Handle, on IdP side, OAUTH2 / OpenID-Connect1 check_id requests.
 *
 * return:: a string (such as Location: header) and let the caller output it.
 *     Sometimes a dummy string is just output to indicate status, e.g.
 *     "O" for SSO OK, "K" for normal OK no further action needed,
 *     "M" show management screen, "I" forward to IdP dispatch, or
 *     "* ERR" for error situations. These special strings
 *     are allocated from static storage and MUST NOT be freed. Other
 *     strings such as "Location: ..." should be freed by caller. */

/* Called by:  zxid_simple_no_ses_cf */
char* zxid_idp_oauth2_check_id(zxid_conf* cf, zxid_cgi* cgi, zxid_ses* ses, int auto_flags)
{
  int ret = 0;

  if (cgi->id_token) {  /* OAUTH2 artifact / redir biding, aka OpenID-Connect1 */
    /* The id_token is directly the local filename of the corresponsing assertion. */
    
    D("ret=%d ses=%p", ret, ses);

    //return zxid_simple_show_page(cf, ss, ZXID_AUTO_METAC, ZXID_AUTO_METAH, "b", "text/xml", res_len, auto_flags);
  }
  
  if (cf->log_level > 0)
    zxlog(cf, 0, 0, 0, 0, 0, 0, ZX_GET_CONTENT(ses->nameid), "N", "C", "IDPOACI", 0, "sid(%s) unknown req or resp", STRNULLCHK(ses->sid));
  ERR("Unknown request or response %d", 0);
  return 0;
}

/* EOF  --  zxidoauth.c */
