Skip to content

Instantly share code, notes, and snippets.

@osmyn
Created July 1, 2017 20:44
Show Gist options
  • Save osmyn/df62e8926b8029ccfe792fa54b59eff1 to your computer and use it in GitHub Desktop.
Save osmyn/df62e8926b8029ccfe792fa54b59eff1 to your computer and use it in GitHub Desktop.
Alexa Skill Validators
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
namespace OneHundredCalories.Models
{
[JsonObject]
public class AlexaRequest
{
[JsonProperty("version")]
public string Version { get; set; }
[JsonProperty("session")]
public SessionAttributes Session { get; set; }
[JsonProperty("request")]
public RequestAttributes Request { get; set; }
[JsonObject("attributes")]
public class SessionCustomAttributes
{
[JsonProperty("memberId")]
public int MemberId { get; set; }
}
[JsonObject("session")]
public class SessionAttributes
{
[JsonProperty("sessionId")]
public string SessionId { get; set; }
[JsonProperty("application")]
public ApplicationAttributes Application { get; set; }
[JsonProperty("attributes")]
public SessionCustomAttributes Attributes { get; set; }
[JsonProperty("user")]
public UserAttributes User { get; set; }
[JsonProperty("new")]
public bool New { get; set; }
[JsonObject("application")]
public class ApplicationAttributes
{
[JsonProperty("applicationId")]
public string ApplicationId { get; set; }
}
[JsonObject("user")]
public class UserAttributes
{
[JsonProperty("userId")]
public string UserId { get; set; }
[JsonProperty("accessToken")]
public string AccessToken { get; set; }
}
}
[JsonObject("request")]
public class RequestAttributes
{
private string _timestampEpoch;
private double _timestamp;
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("requestId")]
public string RequestId { get; set; }
[JsonProperty("timestamp")]
public string TimestampEpoch
{
get
{
return _timestampEpoch;
}
set
{
_timestampEpoch = value;
if (Double.TryParse(value, out _timestamp) && _timestamp > 0)
Timestamp = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(_timestamp);
else
{
var timeStamp = DateTime.MinValue;
if (DateTime.TryParse(_timestampEpoch, out timeStamp))
Timestamp = timeStamp.ToUniversalTime();
}
}
}
[JsonIgnore]
public DateTime Timestamp { get; set; }
[JsonProperty("intent")]
public IntentAttributes Intent { get; set; }
[JsonProperty("reason")]
public string Reason { get; set; }
public RequestAttributes()
{
Intent = new IntentAttributes();
}
[JsonObject("intent")]
public class IntentAttributes
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("slots")]
public dynamic Slots { get; set; }
public List<KeyValuePair<string, string>> GetSlots()
{
var output = new List<KeyValuePair<string, string>>();
if (Slots == null) return output;
foreach (var slot in Slots.Children())
{
if (slot.First.value != null)
output.Add(new KeyValuePair<string, string> (slot.First.name.ToString(), slot.First.value.ToString()));
}
return output;
}
}
}
}
}
using System;
using Newtonsoft.Json;
using OneHundredCalories.Dal.Models;
namespace OneHundredCalories.Models
{
[JsonObject]
public class AlexaResponse
{
[JsonProperty("version")]
public string Version { get; set; }
[JsonProperty("sessionAttributes")]
public SessionAttributes Session { get; set; }
[JsonProperty("response")]
public ResponseAttributes Response { get; set; }
public AlexaResponse()
{
Version = "1.0";
Session = new SessionAttributes();
Response = new ResponseAttributes();
}
public AlexaResponse(string outputSpeechText)
: this()
{
Response.OutputSpeech.Text = outputSpeechText;
Response.Card.Content = outputSpeechText;
}
public AlexaResponse(string outputSpeechText, bool shouldEndSession)
: this()
{
Response.OutputSpeech.Text = outputSpeechText;
Response.ShouldEndSession = shouldEndSession;
if (shouldEndSession)
{
Response.Card.Content = outputSpeechText;
}
else
{
Response.Card = null;
}
}
public AlexaResponse(string outputSpeechText, string cardContent)
: this()
{
Response.OutputSpeech.Text = outputSpeechText;
Response.Card.Content = cardContent;
}
[JsonObject("sessionAttributes")]
public class SessionAttributes
{
[JsonProperty("memberId")]
public int MemberId { get; set; }
}
[JsonObject("response")]
public class ResponseAttributes
{
[JsonProperty("shouldEndSession")]
public bool ShouldEndSession { get; set; }
[JsonProperty("outputSpeech")]
public OutputSpeechAttributes OutputSpeech { get; set; }
[JsonProperty("card")]
public CardAttributes Card { get; set; }
[JsonProperty("reprompt")]
public RepromptAttributes Reprompt { get; set; }
public ResponseAttributes()
{
ShouldEndSession = true;
OutputSpeech = new OutputSpeechAttributes();
Card = new CardAttributes();
Reprompt = new RepromptAttributes();
}
[JsonObject("outputSpeech")]
public class OutputSpeechAttributes
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("text")]
public string Text { get; set; }
[JsonProperty("ssml")]
public string Ssml { get; set; }
public OutputSpeechAttributes()
{
Type = "PlainText";
}
}
[JsonObject("card")]
public class CardAttributes
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("content")]
public string Content { get; set; }
public CardAttributes()
{
Type = "Simple";
}
}
[JsonObject("reprompt")]
public class RepromptAttributes
{
[JsonProperty("outputSpeech")]
public OutputSpeechAttributes OutputSpeech { get; set; }
public RepromptAttributes()
{
OutputSpeech = new OutputSpeechAttributes();
}
}
}
public void HelpIntentHandler(Request request)
{
throw new NotImplementedException();
}
}
}
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using OneHundredCalories.Helpers;
namespace OneHundredCalories.Models
{
public class AlexaRequestValidationHandler : System.Net.Http.DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var body = request.Content.ReadAsStringAsync().Result;
if (!request.Headers.Contains("Signature") || !request.Headers.Contains("SignatureCertChainUrl"))
{
try
{
LogHelper.BadRequest("Bad signature: " +
Newtonsoft.Json.JsonConvert.SerializeObject(request.Headers));
}
catch (Exception ex)
{
LogHelper.BadRequest("Bad signature, json failed");
}
}
var signatureCertChainUrl = request.Headers.GetValues("SignatureCertChainUrl")
.First()
.Replace("/../", "/");
if (string.IsNullOrWhiteSpace(signatureCertChainUrl))
{
LogHelper.BadRequest("No signature cert chain url");
}
var certUrl = new Uri(signatureCertChainUrl);
if (!((certUrl.Port == 443 || !certUrl.IsDefaultPort)
&& certUrl.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)
&& certUrl.Host.Equals("s3.amazonaws.com")
&& certUrl.AbsolutePath.StartsWith("/echo.api/")))
LogHelper.BadRequest(string.Join(",",
"bad port, scheme, host, or path: ",
certUrl.Port,
certUrl.IsDefaultPort,
certUrl.Scheme,
certUrl.Host,
certUrl.AbsolutePath));
using (var web = new WebClient())
{
byte[] certificate = web.DownloadData(certUrl);
var cert = new X509Certificate2(certificate);
var effectiveDate = DateTime.MinValue;
var expiryDate = DateTime.MinValue;
var hasSubject = cert.Subject.Contains("CN=echo-api.amazon.com");
//Verify that the signing certificate has not expired (examine both the Not Before and Not After dates)
if ((DateTime.TryParse(cert.GetExpirationDateString(), out expiryDate)
&& expiryDate > DateTime.UtcNow)
&& hasSubject
&& (DateTime.TryParse(cert.GetEffectiveDateString(), out effectiveDate)
&& effectiveDate < DateTime.UtcNow))
{
//Base64 decode the Signature header value on the request to obtain the encrypted signature.
var signatureString = request.Headers.GetValues("Signature").First();
byte[] signature = Convert.FromBase64String(signatureString);
using (var sha1 = new SHA1Managed())
{
var data = sha1.ComputeHash(Encoding.UTF8.GetBytes(body));
var rsa = (RSACryptoServiceProvider)cert.PublicKey.Key;
if (rsa != null)
{
//Compare the asserted hash value and derived hash values to ensure that they match.
if (!rsa.VerifyHash(data, CryptoConfig.MapNameToOID("SHA1"), signature))
{
LogHelper.BadRequest("bad rsa ");
}
}
}
}
}
return base.SendAsync(request, cancellationToken);
}
}
}
public void ValidateRequest(AlexaRequest alexaRequest)
{
if (alexaRequest.Session.Application.ApplicationId != _applicationId)
LogHelper.BadRequest("bad application id " + alexaRequest.Session.Application.ApplicationId);
var totalSeconds = (DateTime.UtcNow - alexaRequest.Request.Timestamp).TotalSeconds;
if (totalSeconds <= 0 || totalSeconds > 150)
LogHelper.BadRequest("Invalid time window " + totalSeconds);
}
using System.Net;
using System.Net.Http;
using System.Web.Http;
using OneHundredCalories.Dal;
using OneHundredCalories.Dal.Models;
namespace OneHundredCalories.Helpers
{
public class LogHelper
{
public static void BadRequest(string message)
{
var repo = new EFRepository<OhcContext>(new OhcContext());
var log = new Log()
{
Message = message
};
repo.Create(log);
repo.Save();
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.BadRequest));
}
public static void Error(string message)
{
var repo = new EFRepository<OhcContext>(new OhcContext());
var log = new Log()
{
Message = message
};
repo.Create(log);
repo.Save();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment