-
-
Save marfarma/5534218 to your computer and use it in GitHub Desktop.
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 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] ?? ""; | |
} | |
} | |
} |
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
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