PinLoginService.cs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Globalization;
  4. using System.Threading.Tasks;
  5. using MediaBrowser.Common.Extensions;
  6. using MediaBrowser.Controller.Library;
  7. using MediaBrowser.Controller.Net;
  8. using MediaBrowser.Controller.Session;
  9. using MediaBrowser.Model.Connect;
  10. using MediaBrowser.Model.Dto;
  11. using MediaBrowser.Model.Session;
  12. using ServiceStack;
  13. namespace MediaBrowser.Api
  14. {
  15. [Route("/Auth/Pin", "POST", Summary = "Creates a pin request")]
  16. public class CreatePinRequest : IReturn<PinCreationResult>
  17. {
  18. [ApiMember(Name = "DeviceId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
  19. public string DeviceId { get; set; }
  20. [ApiMember(Name = "AppName", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
  21. public string AppName { get; set; }
  22. }
  23. [Route("/Auth/Pin", "GET", Summary = "Gets pin status")]
  24. public class GetPinStatusRequest : IReturn<PinStatusResult>
  25. {
  26. [ApiMember(Name = "DeviceId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
  27. public string DeviceId { get; set; }
  28. [ApiMember(Name = "Pin", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
  29. public string Pin { get; set; }
  30. }
  31. [Route("/Auth/Pin/Exchange", "POST", Summary = "Exchanges a pin")]
  32. public class ExchangePinRequest : IReturn<PinExchangeResult>
  33. {
  34. [ApiMember(Name = "DeviceId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
  35. public string DeviceId { get; set; }
  36. [ApiMember(Name = "Pin", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
  37. public string Pin { get; set; }
  38. }
  39. [Route("/Auth/Pin/Validate", "POST", Summary = "Validates a pin")]
  40. [Authenticated]
  41. public class ValidatePinRequest : IReturn<SessionInfoDto>
  42. {
  43. [ApiMember(Name = "Pin", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
  44. public string Pin { get; set; }
  45. }
  46. public class PinLoginService : BaseApiService
  47. {
  48. private static readonly ConcurrentDictionary<string, MyPinStatus> _activeRequests = new ConcurrentDictionary<string, MyPinStatus>(StringComparer.OrdinalIgnoreCase);
  49. private readonly ISessionManager _sessionManager;
  50. private readonly IUserManager _userManager;
  51. public PinLoginService(ISessionManager sessionManager, IUserManager userManager)
  52. {
  53. _sessionManager = sessionManager;
  54. _userManager = userManager;
  55. }
  56. public object Post(CreatePinRequest request)
  57. {
  58. if (string.IsNullOrWhiteSpace(request.DeviceId))
  59. {
  60. throw new ArgumentNullException("DeviceId");
  61. }
  62. if (string.IsNullOrWhiteSpace(request.AppName))
  63. {
  64. throw new ArgumentNullException("AppName");
  65. }
  66. var pin = GetNewPin();
  67. var value = new MyPinStatus
  68. {
  69. CreationTimeUtc = DateTime.UtcNow,
  70. IsConfirmed = false,
  71. IsExpired = false,
  72. Pin = pin,
  73. DeviceId = request.DeviceId,
  74. AppName = request.AppName
  75. };
  76. _activeRequests.AddOrUpdate(pin, value, (k, v) => value);
  77. return ToOptimizedResult(new PinCreationResult
  78. {
  79. DeviceId = request.DeviceId,
  80. IsConfirmed = false,
  81. IsExpired = false,
  82. Pin = pin
  83. });
  84. }
  85. public object Get(GetPinStatusRequest request)
  86. {
  87. MyPinStatus status;
  88. if (!_activeRequests.TryGetValue(request.Pin, out status))
  89. {
  90. Logger.Debug("Pin {0} not found.", request.Pin);
  91. throw new ResourceNotFoundException();
  92. }
  93. EnsureValid(request.DeviceId, status);
  94. return ToOptimizedResult(new PinStatusResult
  95. {
  96. Pin = status.Pin,
  97. IsConfirmed = status.IsConfirmed,
  98. IsExpired = status.IsExpired
  99. });
  100. }
  101. public async Task<object> Post(ExchangePinRequest request)
  102. {
  103. MyPinStatus status;
  104. if (!_activeRequests.TryGetValue(request.Pin, out status))
  105. {
  106. Logger.Debug("Pin {0} not found.", request.Pin);
  107. throw new ResourceNotFoundException();
  108. }
  109. EnsureValid(request.DeviceId, status);
  110. if (!status.IsConfirmed)
  111. {
  112. throw new ResourceNotFoundException();
  113. }
  114. var auth = AuthorizationContext.GetAuthorizationInfo(Request);
  115. var user = _userManager.GetUserById(status.UserId);
  116. var result = await _sessionManager.CreateNewSession(new AuthenticationRequest
  117. {
  118. App = auth.Client,
  119. AppVersion = auth.Version,
  120. DeviceId = auth.DeviceId,
  121. DeviceName = auth.Device,
  122. RemoteEndPoint = Request.RemoteIp,
  123. Username = user.Name
  124. }).ConfigureAwait(false);
  125. return ToOptimizedResult(result);
  126. }
  127. public object Post(ValidatePinRequest request)
  128. {
  129. MyPinStatus status;
  130. if (!_activeRequests.TryGetValue(request.Pin, out status))
  131. {
  132. throw new ResourceNotFoundException();
  133. }
  134. EnsureValid(status);
  135. status.IsConfirmed = true;
  136. status.UserId = AuthorizationContext.GetAuthorizationInfo(Request).UserId;
  137. return ToOptimizedResult(new ValidatePinResult
  138. {
  139. AppName = status.AppName
  140. });
  141. }
  142. private void EnsureValid(string requestedDeviceId, MyPinStatus status)
  143. {
  144. if (!string.Equals(requestedDeviceId, status.DeviceId, StringComparison.OrdinalIgnoreCase))
  145. {
  146. Logger.Debug("Pin device Id's do not match. requestedDeviceId: {0}, status.DeviceId: {1}", requestedDeviceId, status.DeviceId);
  147. throw new ResourceNotFoundException();
  148. }
  149. EnsureValid(status);
  150. }
  151. private void EnsureValid(MyPinStatus status)
  152. {
  153. if ((DateTime.UtcNow - status.CreationTimeUtc).TotalMinutes > 10)
  154. {
  155. status.IsExpired = true;
  156. }
  157. if (status.IsExpired)
  158. {
  159. Logger.Debug("Pin {0} is expired", status.Pin);
  160. throw new ResourceNotFoundException();
  161. }
  162. }
  163. private string GetNewPin()
  164. {
  165. var pin = GetNewPinInternal();
  166. while (IsPinActive(pin))
  167. {
  168. pin = GetNewPinInternal();
  169. }
  170. return pin;
  171. }
  172. private string GetNewPinInternal()
  173. {
  174. return new Random().Next(10000, 99999).ToString(CultureInfo.InvariantCulture);
  175. }
  176. private bool IsPinActive(string pin)
  177. {
  178. MyPinStatus status;
  179. if (!_activeRequests.TryGetValue(pin, out status))
  180. {
  181. return false;
  182. }
  183. if (status.IsExpired)
  184. {
  185. return false;
  186. }
  187. return true;
  188. }
  189. public class MyPinStatus : PinStatusResult
  190. {
  191. public DateTime CreationTimeUtc { get; set; }
  192. public string DeviceId { get; set; }
  193. public string UserId { get; set; }
  194. public string AppName { get; set; }
  195. }
  196. }
  197. public class ValidatePinResult
  198. {
  199. public string AppName { get; set; }
  200. }
  201. }