using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.Owin.Infrastructure; using Microsoft.Owin.Logging; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Infrastructure; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Text; namespace Owin.Security.Providers.PayPal { public class PayPalAuthenticationHandler : AuthenticationHandler { private const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string"; private readonly ILogger _logger; private readonly HttpClient _httpClient; public PayPalAuthenticationHandler(HttpClient httpClient, ILogger logger) { _httpClient = httpClient; _logger = logger; } protected override async Task AuthenticateCoreAsync() { AuthenticationProperties properties = null; var error = "Unknown"; try { string code = null; string state = null; var query = Request.Query; var values = query.GetValues("code"); if(values != null && values.Count == 1) { code = values[0]; } values = query.GetValues("error"); if(values != null && values.Count == 1) { error = values[0]; } values = query.GetValues("state"); if(values != null && values.Count == 1) { state = values[0]; } properties = Options.StateDataFormat.Unprotect(state); if(properties == null) { return null; } // OAuth2 10.12 CSRF if(!ValidateCorrelationId(properties, _logger)) { return new AuthenticationTicket(null, properties); } var requestPrefix = Request.Scheme + "://" + Request.Host; var redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath; // Request the token var requestMessage = new HttpRequestMessage(HttpMethod.Post, Options.Endpoints.TokenEndpoint); requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes( $"{Options.ClientId}:{Options.ClientSecret}"))); requestMessage.Content = new FormUrlEncodedContent(new List>{ new KeyValuePair("grant_type", "authorization_code"), new KeyValuePair("code", code), new KeyValuePair("redirect_uri", redirectUri), }); string text = ""; try { var tokenResponse = await _httpClient.SendAsync(requestMessage); tokenResponse.EnsureSuccessStatusCode(); text = await tokenResponse.Content.ReadAsStringAsync(); } catch (HttpRequestException ex) { if (ex.InnerException is System.Net.WebException && ex.InnerException.Message.Contains("TLS")) { if (!System.Net.ServicePointManager.SecurityProtocol.HasFlag(System.Net.SecurityProtocolType.Tls12)) { throw new System.Net.WebException("PayPal requires TLS v1.2. TLS v1.0 and v1.1 connections will be refused. Set System.Net.ServicePointManager.SecurityProtocol = System.Net.ServicePointManager.SecurityProtocol | System.Net.SecurityProtocolType.Tls12", ex.InnerException); } } throw; } // Deserializes the token response var response = JsonConvert.DeserializeObject(text); var accessToken = (string)response.access_token; var refreshToken = (string)response.refresh_token; // Get the PayPal user var userRequest = new HttpRequestMessage(HttpMethod.Get, Options.Endpoints.UserInfoEndpoint); userRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); userRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var userResponse = await _httpClient.SendAsync(userRequest, Request.CallCancelled); userResponse.EnsureSuccessStatusCode(); text = await userResponse.Content.ReadAsStringAsync(); var user = JObject.Parse(text); var context = new PayPalAuthenticatedContext(Context, user, accessToken, refreshToken) { Identity = new ClaimsIdentity(Options.AuthenticationType, ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType) }; if(!string.IsNullOrEmpty(context.Id)) { context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, XmlSchemaString, Options.AuthenticationType)); } if(!string.IsNullOrEmpty(context.Email)) { context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, XmlSchemaString, Options.AuthenticationType)); } if(!string.IsNullOrEmpty(context.Name)) { context.Identity.AddClaim(new Claim("urn:paypal:name", context.Name, XmlSchemaString, Options.AuthenticationType)); } context.Properties = properties; await Options.Provider.Authenticated(context); return new AuthenticationTicket(context.Identity, context.Properties); } catch(Exception ex) { _logger.WriteError(ex.Message + " : " + error); } return new AuthenticationTicket(null, properties); } protected override Task ApplyResponseChallengeAsync() { if(Response.StatusCode != 401) { return Task.FromResult(null); } var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode); if (challenge == null) return Task.FromResult(null); var baseUri = Request.Scheme + Uri.SchemeDelimiter + Request.Host + Request.PathBase; var currentUri = baseUri + Request.Path + Request.QueryString; var redirectUri = baseUri + Options.CallbackPath; var properties = challenge.Properties; if(string.IsNullOrEmpty(properties.RedirectUri)) { properties.RedirectUri = currentUri; } // OAuth2 10.12 CSRF GenerateCorrelationId(properties); // comma separated var scope = string.Join(" ", Options.Scope); var state = Options.StateDataFormat.Protect(properties); var authorizationEndpoint = Options.Endpoints.AuthorizationEndpoint + "?client_id=" + Uri.EscapeDataString(Options.ClientId) + "&redirect_uri=" + Uri.EscapeDataString(redirectUri) + "&scope=" + Uri.EscapeDataString(scope) + "&response_type=code" + "&state=" + Uri.EscapeDataString(state); Response.Redirect(authorizationEndpoint); return Task.FromResult(null); } public override async Task InvokeAsync() { return await InvokeReplyPathAsync(); } private async Task InvokeReplyPathAsync() { if (!Options.CallbackPath.HasValue || Options.CallbackPath != Request.Path) return false; var ticket = await AuthenticateAsync(); if(ticket == null) { _logger.WriteWarning("Invalid return state, unable to redirect."); Response.StatusCode = 500; return true; } var context = new PayPalReturnEndpointContext(Context, ticket) { SignInAsAuthenticationType = Options.SignInAsAuthenticationType, RedirectUri = ticket.Properties.RedirectUri }; await Options.Provider.ReturnEndpoint(context); if(context.SignInAsAuthenticationType != null && context.Identity != null) { var grantIdentity = context.Identity; if(!string.Equals(grantIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal)) { grantIdentity = new ClaimsIdentity(grantIdentity.Claims, context.SignInAsAuthenticationType, grantIdentity.NameClaimType, grantIdentity.RoleClaimType); } Context.Authentication.SignIn(context.Properties, grantIdentity); } if (context.IsRequestCompleted || context.RedirectUri == null) return context.IsRequestCompleted; var redirectUri = context.RedirectUri; if(context.Identity == null) { // add a redirect hint that sign-in failed in some way redirectUri = WebUtilities.AddQueryString(redirectUri, "error", "access_denied"); } Response.Redirect(redirectUri); context.RequestCompleted(); return context.IsRequestCompleted; } } }