using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.Owin; using Microsoft.Owin.Infrastructure; using Microsoft.Owin.Logging; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Infrastructure; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Owin.Security.Providers.LinkedIn { public class LinkedInAuthenticationHandler : AuthenticationHandler { private const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string"; private const string TokenEndpoint = "https://www.linkedin.com/uas/oauth2/accessToken"; private const string UserInfoEndpoint = "https://api.linkedin.com/v1/people/"; private readonly ILogger logger; private readonly HttpClient httpClient; public LinkedInAuthenticationHandler(HttpClient httpClient, ILogger logger) { this.httpClient = httpClient; this.logger = logger; } protected override async Task AuthenticateCoreAsync() { AuthenticationProperties properties = null; try { string code = null; string state = null; IReadableStringCollection query = Request.Query; IList values = query.GetValues("code"); if (values != null && values.Count == 1) { code = 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); } string requestPrefix = Request.Scheme + "://" + Request.Host; string redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath; // Build up the body for the token request var body = new List>(); body.Add(new KeyValuePair("grant_type", "authorization_code")); body.Add(new KeyValuePair("code", code)); body.Add(new KeyValuePair("redirect_uri", redirectUri)); body.Add(new KeyValuePair("client_id", Options.ClientId)); body.Add(new KeyValuePair("client_secret", Options.ClientSecret)); // Request the token HttpResponseMessage tokenResponse = await httpClient.PostAsync(TokenEndpoint, new FormUrlEncodedContent(body)); tokenResponse.EnsureSuccessStatusCode(); string text = await tokenResponse.Content.ReadAsStringAsync(); // Deserializes the token response dynamic response = JsonConvert.DeserializeObject(text); string accessToken = (string)response.access_token; string expires = (string) response.expires_in; // Get the LinkedIn user string userInfoEndpoint = UserInfoEndpoint + "~:("+ string.Join(",", Options.ProfileFields.Distinct().ToArray()) +")" + "?oauth2_access_token=" + Uri.EscapeDataString(accessToken); HttpRequestMessage userRequest = new HttpRequestMessage(HttpMethod.Get, userInfoEndpoint); userRequest.Headers.Add("x-li-format", "json"); HttpResponseMessage graphResponse = await httpClient.SendAsync(userRequest, Request.CallCancelled); graphResponse.EnsureSuccessStatusCode(); text = await graphResponse.Content.ReadAsStringAsync(); JObject user = JObject.Parse(text); var context = new LinkedInAuthenticatedContext(Context, user, accessToken, expires); context.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.GivenName)) { context.Identity.AddClaim(new Claim(ClaimTypes.GivenName, context.GivenName, XmlSchemaString, Options.AuthenticationType)); } if (!string.IsNullOrEmpty(context.FamilyName)) { context.Identity.AddClaim(new Claim(ClaimTypes.Surname, context.FamilyName, XmlSchemaString, Options.AuthenticationType)); } if (!string.IsNullOrEmpty(context.Name)) { context.Identity.AddClaim(new Claim(ClaimTypes.Name, context.Name, XmlSchemaString, Options.AuthenticationType)); context.Identity.AddClaim(new Claim("urn:linkedin:name", context.Name, XmlSchemaString, Options.AuthenticationType)); } if (!string.IsNullOrEmpty(context.Link)) { context.Identity.AddClaim(new Claim("urn:linkedin:url", context.Link, XmlSchemaString, Options.AuthenticationType)); } if (!string.IsNullOrEmpty(context.AccessToken)) { context.Identity.AddClaim(new Claim("urn:linkedin:accesstoken", context.AccessToken, 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); } return new AuthenticationTicket(null, properties); } protected override Task ApplyResponseChallengeAsync() { if (Response.StatusCode != 401) { return Task.FromResult(null); } AuthenticationResponseChallenge challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode); if (challenge != null) { string baseUri = Request.Scheme + Uri.SchemeDelimiter + Request.Host + Request.PathBase; string currentUri = baseUri + Request.Path + Request.QueryString; string redirectUri = baseUri + Options.CallbackPath; AuthenticationProperties properties = challenge.Properties; if (string.IsNullOrEmpty(properties.RedirectUri)) { properties.RedirectUri = currentUri; } // OAuth2 10.12 CSRF GenerateCorrelationId(properties); // comma separated string scope = string.Join(",", Options.Scope); // allow scopes to be specified via the authentication properties for this request, when specified they will already be comma separated if (properties.Dictionary.ContainsKey("scope")) { scope = properties.Dictionary["scope"]; } string state = Options.StateDataFormat.Protect(properties); string authorizationEndpoint = "https://www.linkedin.com/uas/oauth2/authorization" + "?response_type=code" + "&client_id=" + Uri.EscapeDataString(Options.ClientId) + "&redirect_uri=" + Uri.EscapeDataString(redirectUri) + "&scope=" + Uri.EscapeDataString(scope) + "&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) { // TODO: error responses AuthenticationTicket ticket = await AuthenticateAsync(); if (ticket == null) { logger.WriteWarning("Invalid return state, unable to redirect."); Response.StatusCode = 500; return true; } var context = new LinkedInReturnEndpointContext(Context, ticket); context.SignInAsAuthenticationType = Options.SignInAsAuthenticationType; context.RedirectUri = ticket.Properties.RedirectUri; await Options.Provider.ReturnEndpoint(context); if (context.SignInAsAuthenticationType != null && context.Identity != null) { ClaimsIdentity 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) { string 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; } return false; } } }