Controllers
ChatAuthController.cs
/*
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License. See License.txt in the project root for license information.
*/
// --------------------------------------------------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// --------------------------------------------------------------------------------------------------------------------
namespace Site.Areas.Chat.Controllers
{
using System;
using System.Collections.Generic;
using System.IdensatyModel.Tokens;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Web.Mvc;
using Adxstudio.Xrm;
using Adxstudio.Xrm.Configuration;
using Microsoft.Xrm.Portal.Configuration;
using Microsoft.Xrm.Sdk;
using Models;
using Models.LivePerson;
using Newtonsoft.Json;
using Org.BouncyCastle.OpenSsl;
///
/// The chat auth controller.
///
public clast ChatAuthController : Controller
{
///
/// Retrieve Portal Public Key
///
/// Encoded public key
[HttpGet]
[AllowAnonymous]
public ActionResult PublicKey()
{
using (var cryptoServiceProvider = ChatAuthController.GetCryptoProvider(false))
{
var stringWriter = new StringWriter();
ChatAuthController.ExportPublicKey(cryptoServiceProvider, stringWriter);
return this.Content(stringWriter.ToString(), "text/plain");
}
}
///
/// Return JWT Auth token for current user to provide authenticated data to Chat
///
/// JWT Auth token for current user
[HttpGet]
public ActionResult Token()
{
IList claims;
if (this.HttpContext.User.Idensaty.IsAuthenticated)
{
claims = this.GetUserClaims();
}
else
{
ADXTrace.Instance.TraceInfo(TraceCategory.Application, "User not logged in");
return new HttpStatusCodeResult(HttpStatusCode.Unauthorized);
}
var tokenString = GetTokenString(claims);
return this.Content(tokenString, "application/jwt");
}
///
/// Endpoint for 3rd-party chat provider to use to get JWT auth token. May require user to login.
///
/// sresponse type
/// client id
/// redirect uri
/// OAuth scope
/// OAuth state
/// nonce value
/// redirect with JWT
[AllowAnonymous]
public ActionResult Authorize(string response_type, string client_id, string redirect_uri, string scope,
string state, string nonce)
{
if (string.IsNullOrEmpty(response_type) ||
response_type.Split(' ')
.All(s => string.Compare("token", s, StringComparison.InvariantCultureIgnoreCase) != 0))
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
if (this.HttpContext.User.Idensaty.IsAuthenticated)
{
ADXTrace.Instance.TraceInfo(TraceCategory.Application, "Authenticated User, returning token");
var claims = this.GetUserClaims();
if (!string.IsNullOrEmpty(nonce))
{
claims.Add(new Claim("nonce", nonce));
}
var token = GetTokenString(claims);
var url = new UriBuilder(redirect_uri);
var qs = !string.IsNullOrEmpty(url.Query) && url.Query.Length > 1 ? url.Query.Substring(1) + "&" : string.Empty;
qs += "token=" + token; // token is already encoded
url.Query = qs;
return this.Redirect(url.ToString());
}
else
{
ADXTrace.Instance.TraceInfo(TraceCategory.Application, "Unauthenticated User, triggering authentication");
var urlState = EncodeState(Request.Url.AbsoluteUri);
var url = Url.Action("AuthForce", new { state = urlState });
return this.Redirect(url);
}
}
///
/// Force authentication and redirect based on compressed url in state
///
/// compressed url
/// redirect to token generator
public ActionResult AuthForce(string state)
{
if (this.HttpContext.User.Idensaty.IsAuthenticated)
{
if (!string.IsNullOrEmpty(state))
{
var returnUrl = DecodeState(state);
return this.Redirect(returnUrl);
}
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
else
{
return new HttpStatusCodeResult(HttpStatusCode.Unauthorized);
}
}
///
/// Compress and encode string for url
///
/// string to encode
/// encoded string
private static string EncodeState(string state)
{
var bytes = Encoding.UTF8.GetBytes(state);
using (var input = new MemoryStream(bytes))
using (var output = new MemoryStream())
{
using (var zip = new GZipStream(output, CompressionMode.Compress))
{
input.CopyTo(zip);
}
return Base64UrlEncoder.Encode(output.ToArray());
}
}
///
/// Decompress and decode string from url
///
/// encoded string
/// decoded string
private static string DecodeState(string encodedState)
{
var bytes = Base64UrlEncoder.DecodeBytes(encodedState);
using (var input = new MemoryStream(bytes))
using (var output = new MemoryStream())
{
using (var zip = new GZipStream(input, CompressionMode.Decompress))
{
zip.CopyTo(output);
}
return Encoding.UTF8.GetString(output.ToArray());
}
}
///
/// Export a public key in PEM format
///
/// Crypto Service Provider with key to export
/// output stream to write key to
private static void ExportPublicKey(RSACryptoServiceProvider csp, TextWriter outputStream)
{
var keyParams = csp.ExportParameters(false);
var publicKey = Org.BouncyCastle.Security.DotNetUtilities.GetRsaPublicKey(keyParams);
PemWriter pemWriter = new PemWriter(outputStream);
pemWriter.WriteObject(publicKey);
}
///
/// Get a crypto service provider for portal keys
///
/// flag indicating whether to include private key in provider
/// RSACryptoServiceProvider initialized with appropriate keys
private static RSACryptoServiceProvider GetCryptoProvider(bool includePrivateKey)
{
var cert = PortalSettings.Instance.Certificate.FindCertificates().FirstOrDefault();
if (cert == null)
{
ADXTrace.Instance.TraceError(TraceCategory.Application, "Unable to find a valid certificate");
throw new CertificateNotFoundException();
}
var csp = includePrivateKey
? (RSACryptoServiceProvider)cert.PrivateKey
: (RSACryptoServiceProvider)cert.PublicKey.Key;
if (includePrivateKey)
{
// need to move key into enhanced crypto provider without exporting key
var rsa256Csp = new RSACryptoServiceProvider().CspKeyContainerInfo;
var cspParams = new CspParameters(rsa256Csp.ProviderType, rsa256Csp.ProviderName,
csp.CspKeyContainerInfo.KeyContainerName);
return new RSACryptoServiceProvider(2048, cspParams) { PersistKeyInCsp = true };
}
else
{
return csp;
}
}
///
/// Get encoded JWT token for given claims
///
/// list of claims to include in token
/// string representation of JWT token
private static string GetTokenString(IList claims)
{
string tokenString = null;
using (var cryptoServiceProvider = GetCryptoProvider(true))
{
string issuer = PortalSettings.Instance.DomainName;
string audience = string.Empty;
DateTime notBefore = DateTime.Now;
DateTime expires = notBefore.AddHours(1);
var tokenHandler = new JwtSecurityTokenHandler();
var signingCredentials = new SigningCredentials(new RsaSecurityKey(cryptoServiceProvider),
SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest);
// need to explicitly add "iat" claim
DateTime unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
var iat = Convert.ToInt64((TimeZoneInfo.ConvertTimeToUtc(notBefore) - unixEpoch).TotalSeconds - 1);
claims.Add(new Claim("iat", iat.ToString(), ClaimValueTypes.Integer));
var header = new JwtHeader(signingCredentials);
var payload = new JwtPayload(issuer, audience, claims, notBefore, expires);
// Need to adjust this because Claim clast ignores value type
payload["iat"] = Convert.ToInt64(payload["iat"]);
var jwtToken = new JwtSecurityToken(header, payload);
tokenString = tokenHandler.WriteToken(jwtToken);
}
return tokenString;
}
///
/// The details of the customer.
///
///
/// The .
///
private ChatUserModel GetChatUserData()
{
var portalContext = PortalCrmConfigurationManager.CreatePortalContext();
if (portalContext.User == null)
{
ADXTrace.Instance.TraceWarning(TraceCategory.Application, "portalContext.User is null");
return null;
}
var result = new ChatUserModel
{
Username = portalContext.User.GetAttributeValue("adx_idensaty_username"),
Id = portalContext.User.GetAttributeValue("contactid"),
FirstName = portalContext.User.GetAttributeValue("firstname"),
LastName = portalContext.User.GetAttributeValue("lastname"),
Email = portalContext.User.GetAttributeValue("emailaddress1"),
Phone = portalContext.User.GetAttributeValue("telephone1")
};
var customerType = portalContext.User.GetAttributeValue("customertypecode");
if (customerType != null)
{
result.CustomerType = customerType.Value;
}
else
{
ADXTrace.Instance.TraceError(TraceCategory.Application, "customertypecode not set");
throw new NullReferenceException("customertypecode");
}
return result;
}
///
/// Get list of claims
///
/// list of claims for user
private IList GetUserClaims()
{
var userData = this.GetChatUserData();
IList claims = new List
{
new Claim("sub", userData.Id.ToString()),
new Claim("preferred_username", userData.Username ?? string.Empty),
new Claim("phone_number", userData.Phone ?? string.Empty),
new Claim("given_name", userData.FirstName ?? string.Empty),
new Claim("family_name", userData.LastName ?? string.Empty),
new Claim("email", userData.Email ?? string.Empty)
};
var lpSdes = new LivePersonSdesCustomerInfo()
{
CustomerInfo = new LivePersonCustomerInfo()
{
Type = "contact",
Id = userData.Id.ToString(),
Imei = userData.Phone ?? string.Empty,
UserName = userData.Username ?? string.Empty
}
};
var custInfo = JsonConvert.SerializeObject(new object[] { lpSdes });
claims.Add(new Claim("lp_sdes", custInfo, "JSON_ARRAY"));
return claims;
}
}
}