Skip to content

Instantly share code, notes, and snippets.

@viktor-evdokimov
Forked from jokecamp/ServiceStackApiHMAC
Last active August 29, 2015 14:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save viktor-evdokimov/f0fdd472df80e26c1fa8 to your computer and use it in GitHub Desktop.
Save viktor-evdokimov/f0fdd472df80e26c1fa8 to your computer and use it in GitHub Desktop.
using ServiceStack.WebHost.Endpoints;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security;
using ServiceStack.Common.Web;
using ServiceStack.Logging;
using ServiceStack.ServiceHost;
using ServiceStack.ServiceInterface;
[assembly: WebActivator.PreApplicationStartMethod(typeof(TeamsApi.App_Start.AppHost), "Start")]
namespace TeamsApi.App_Start
{
public class AppHost : AppHostBase
{
public AppHost() : base("Team API", typeof(TeamService).Assembly)
{
}
public override void Configure(Funq.Container container)
{
//Configure User Defined REST Paths
Routes
.Add<Team>("/teams")
.Add<Team>("teams/{Id}");
// setup mock team data
container.Register(new TeamRepository());
var mockTeams = container.Resolve<TeamRepository>();
mockTeams.Save(new Team() {Id = 1, Name = "Manchester United"});
mockTeams.Save(new Team() { Id = 2, Name = "Arsenal" });
// setup mock users data
container.Register(new UserRepository());
var mockUsers = container.Resolve<UserRepository>();
mockUsers.Save(new User()
{
Id = 1,
Name = "Alex Ferguson",
Secret = "5771CC06-B86D-41A6-AB39-9CA2BA338E27",
IsEnabled = true
});
}
public static void Start()
{
new AppHost().Init();
}
}
public class Team
{
public long Id { get; set; }
public string Name { get; set; }
}
[AuthSignatureRequired]
public class TeamService : RestServiceBase<Team>
{
// Injected by IOC
public TeamRepository Repository { get; set; }
public override object OnGet(Team request)
{
if (request.Id == default(long))
return Repository.GetAll();
return Repository.GetById(request.Id);
}
public override object OnPost(Team team)
{
return Repository.Save(team);
}
public override object OnPut(Team team)
{
return Repository.Save(team);
}
public override object OnDelete(Team request)
{
Repository.DeleteById(request.Id);
return null;
}
}
public class TeamRepository
{
private readonly List<Team> _teams = new List<Team>();
public List<Team> GetAll()
{
return _teams;
}
public Team GetById(long id)
{
return _teams.FirstOrDefault(x => x.Id == id);
}
public Team Save(Team team)
{
if (team.Id == default(long))
{
team.Id = _teams.Count == 0 ? 1 : _teams.Max(x => x.Id) + 1;
}
else
{
for (var i = 0; i < _teams.Count; i++)
{
if (_teams[i].Id != team.Id) continue;
_teams[i] = team;
return team;
}
}
_teams.Add(team);
return team;
}
public void DeleteById(long id)
{
_teams.RemoveAll(x => x.Id == id);
}
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Secret { get; set; }
public bool IsEnabled { get; set; }
}
public class UserRepository
{
private readonly List<User> _users = new List<User>();
public List<User> GetAll()
{
return _users;
}
public User GetById(long id)
{
return _users.FirstOrDefault(x => x.Id == id);
}
public User Save(User team)
{
if (team.Id == default(long))
{
team.Id = _users.Count == 0 ? 1 : _users.Max(x => x.Id) + 1;
}
else
{
for (var i = 0; i < _users.Count; i++)
{
if (_users[i].Id != team.Id) continue;
_users[i] = team;
return team;
}
}
_users.Add(team);
return team;
}
public void DeleteById(long id)
{
_users.RemoveAll(x => x.Id == id);
}
}
public class ApiCustomHttpHeaders
{
public static string UserId = "X-CUSTOM-API-USERID";
public static string Signature = "X-CUSTOM-SIGNATURE";
public static string Date = "X-CUSTOM-DATE";
}
/// <summary>
/// The filter will be execute on every request for every DTO or RestService with this Attribute:
/// </summary>
public class AuthSignatureRequired : ServiceStack.ServiceInterface.RequestFilterAttribute, IHasRequestFilter
{
private static readonly ILog Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public UserRepository UserRepository { get; set; }
public new int Priority
{
// <0 Run before global filters. >=0 Run after
get { return -1; }
}
private bool CanExecute(IHttpRequest req)
{
DateTime requestDate;
if (!DateTime.TryParse(ApiSignature.GetDate(req), out requestDate))
{
throw new SecurityException("You must provide a valid request date in the headers.");
}
var difference = requestDate.Subtract(DateTime.Now);
if (difference.TotalMinutes > 15 || difference.TotalMinutes < -15)
{
throw new SecurityException(string.Format(
"The request timestamp must be within 15 minutes of the server time. Your request is {0} minutes compared to the server. Server time is currently {1} {2}",
difference.TotalMinutes,
DateTime.Now.ToLongDateString(),
DateTime.Now.ToLongTimeString()));
}
var userId = ApiSignature.GetUserId(req);
if (userId <= 0)
{
throw new SecurityException("You must provide a valid API User Id with your request");
}
var signature = ApiSignature.GetSignature(req);
if (string.IsNullOrEmpty(signature))
{
throw new SecurityException("You must provide a valid request signature (hash)");
}
var user = UserRepository.GetById(userId);
if (user == null || user.Id == 0)
{
throw new SecurityException("Your API user id could not be found.");
}
if (!user.IsEnabled)
throw new SecurityException("Your API user account has been disabled.");
if (signature == ApiSignature.CreateToken(req, user.Secret))
{
Logger.InfoFormat("Successfully Authenticated {0}:{1} via signature hash", user.Id, user.Name);
return true;
}
throw new SecurityException("Your request signature (hash) is invalid.");
}
public override void Execute(IHttpRequest req, IHttpResponse res, object requestDto)
{
var authErrorMessage = "";
try
{
// Perform security check
if (CanExecute(req))
return;
}
catch (Exception ex)
{
authErrorMessage = ex.Message;
Logger.ErrorFormat("Blocked unauthorized request: {0} {1} by ip = {2} due to {3}",
req.HttpMethod,
req.AbsoluteUri,
req.UserHostAddress ?? "unknown",
authErrorMessage);
}
// Security failed!
var message = "You are not authorized. " + authErrorMessage;
//throw new HttpError(HttpStatusCode.Unauthorized, message);
res.StatusCode = (int)HttpStatusCode.Unauthorized;
res.StatusDescription = message;
res.AddHeader(HttpHeaders.WwwAuthenticate, string.Format("{0} realm=\"{1}\"", "", "custom api"));
res.ContentType = ContentType.PlainText;
res.Write(message);
res.Close();
}
}
/// <summary>
/// Static class will perform the flattening of the request and creation
/// of the hash. This is designed to be used by the server and could be distributed
/// as part of an SDK. The Test.aspx example uses this class.
/// </summary>
public static class ApiSignature
{
private static readonly ILog Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
/// <summary>
/// Used by SDK and clients to make requests, so we must use the HttpWebRequest class
/// </summary>
/// <param name="webRequest"></param>
/// <param name="secret"></param>
/// <returns></returns>
public static string CreateToken(HttpWebRequest webRequest, string secret)
{
return CreateToken(FlattenRequestDetails(webRequest.Method,
webRequest.RequestUri.AbsoluteUri,
webRequest.ContentType,
webRequest.Date.ToUniversalTime().ToString("r")
), secret);
}
/// <summary>
/// Used by Server so we must use the Service Stack IHttpRequest
/// </summary>
/// <param name="request"></param>
/// <param name="secret"></param>
/// <returns></returns>
public static string CreateToken(IHttpRequest request, string secret)
{
return CreateToken(FlattenRequestDetails(request.HttpMethod,
request.AbsoluteUri,
request.ContentType,
GetDate(request)
), secret);
}
private static string CreateToken(string message, string secret)
{
// don't allow null secrets
secret = secret ?? "";
var encoding = new System.Text.ASCIIEncoding();
byte[] keyByte = encoding.GetBytes(secret);
byte[] messageBytes = encoding.GetBytes(message);
using (var hmacsha256 = new System.Security.Cryptography.HMACSHA256(keyByte))
{
byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
return Convert.ToBase64String(hashmessage);
}
}
private static string FlattenRequestDetails(string httpMethod, string url, string contentType, string date)
{
// If it is a GET then we don't care about the contentType since there will never be contentTypes with GET.
if (httpMethod.ToUpper() == "GET")
contentType = "";
var message = string.Format("{0}{1}{2}{3}", httpMethod, url, contentType, date);
Logger.Debug("Request message to hash: " + message);
return message;
}
/// <summary>
/// If the user is providing the date via the custom header then the server
/// will use that for the hash. Otherwise we check for the default "Date" header.
/// This is nessary since some consumers can't control the date header in their web requests
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public static string GetDate(IHttpRequest request)
{
return request.Headers[ApiCustomHttpHeaders.Date] ?? request.Headers["Date"] ?? "";
}
/// <summary>
/// Public user id to identify the user
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
public static int GetUserId(IHttpRequest req)
{
int userId = 0;
var user = req.Headers[ApiCustomHttpHeaders.UserId] ?? "";
int.TryParse(user, out userId);
return userId;
}
public static string GetSignature(IHttpRequest req)
{
return req.Headers[ApiCustomHttpHeaders.Signature] ?? "";
}
}
}
try
{
var client = new JsonServiceClient();
client.LocalHttpWebRequestFilter +=
delegate(HttpWebRequest request)
{
// ContentType still null at this point so we must hard code it
request.ContentType = ServiceStack.Common.Web.ContentType.Json;
request.Date = DateTime.Now;
var secret = "5771CC06-B86D-41A6-AB39-9CA2BA338E27";
var token = ApiSignature.CreateToken(request, secret);
request.Headers.Add(ApiCustomHttpHeaders.UserId, "1");
request.Headers.Add(ApiCustomHttpHeaders.Signature, token);
};
var teams = client.Get<List<Team>>("http://localhost:59833/api/teams");
foreach (var team in teams)
{
Label1.Text += team.Name + "<br>";
}
}
catch (WebServiceException ex)
{
Label1.Text = ex.Message + " : " + ex.ResponseBody;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment