Created
July 1, 2017 20:44
-
-
Save osmyn/df62e8926b8029ccfe792fa54b59eff1 to your computer and use it in GitHub Desktop.
Alexa Skill Validators
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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