diff --git a/client/package.json b/client/package.json index db31f23..cafc9c9 100644 --- a/client/package.json +++ b/client/package.json @@ -31,13 +31,13 @@ "@qontu/ngx-inline-editor": "^0.2.0-alpha.12", "angular2-jwt": "^0.2.3", "angular2-moment": "^1.8.0", - "angularfire2": "5.0.0-rc.6.0", "angularx-social-login": "^1.1.8", "applicationinsights-js": "^1.0.15", "bootstrap": "4.1.0", "core-js": "^2.5.3", "dropzone": "^5.3.0", - "firebase": "4.13.1", + "firebase":"4.12.1", + "angularfire2":"^5.0.0-rc.6", "font-awesome": "^4.7.0", "howler": "^2.0.9", "jquery": "^3.3.1", diff --git a/server/Controllers/ExternalAuthController.cs b/server/Controllers/ExternalAuthController.cs new file mode 100644 index 0000000..8c3574e --- /dev/null +++ b/server/Controllers/ExternalAuthController.cs @@ -0,0 +1,129 @@ + + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Google.Apis.Auth.OAuth2; +using Google.Apis.Auth.OAuth2.Flows; +using Google.Apis.Auth.OAuth2.Responses; +using Google.Apis.Util.Store; +using Google.Apis.Plus.v1; +using Google.Apis.Plus.v1.Data; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using PodNoms.Api.Models; +using PodNoms.Api.Models.ViewModels; +using PodNoms.Api.Services.Auth; +using PodNoms.Api.Utils; +using Google.Apis.Auth; + +namespace PodNoms.Api.Controllers { + [Route("[controller]/[action]")] + public class ExternalAuthController : Controller { + //TODO: Refactor these somewhere better + public static ClientSecrets secrets = new ClientSecrets() { + ClientSecret = "wPXd9Sw9Z04bnGrobkZoZoGz" + }; + + // Configuration that you probably don't need to change. + static public string APP_NAME = "PodNoms Web API"; + + static public string[] SCOPES = { + PlusService.Scope.PlusLogin, + PlusService.Scope.UserinfoEmail, + PlusService.Scope.UserinfoProfile + }; + private readonly UserManager _userManager; + private readonly FacebookAuthSettings _fbAuthSettings; + private readonly IJwtFactory _jwtFactory; + private readonly JwtIssuerOptions _jwtOptions; + private static readonly HttpClient Client = new HttpClient(); + + public ExternalAuthController(IOptions fbAuthSettingsAccessor, UserManager userManager, + IJwtFactory jwtFactory, IOptions jwtOptions) { + _fbAuthSettings = fbAuthSettingsAccessor.Value; + _userManager = userManager; + _jwtFactory = jwtFactory; + _jwtOptions = jwtOptions.Value; + } + // POST api/externalauth/google + [HttpPost] + public async Task Google([FromBody]SocialAuthViewModel model) { + //1. Validate access token + //2. Generate JWT + //3. Update details + try { + var payload = await GoogleJsonWebSignature.ValidateAsync(model.AccessToken); + return await _processUserDetails(new FacebookUserData { + Email = payload.Email, + FirstName = payload.GivenName, + LastName = payload.FamilyName, + Name = payload.Name, + Picture = new FacebookPictureData { + Data = new FacebookPicture { + Url = payload.Picture + } + } + }); + } catch (Exception ex) { + return BadRequest(Errors.AddErrorToModelState("login_failure", "Invalid google token.", ModelState)); + } + } + + // POST api/externalauth/facebook + [HttpPost] + public async Task Facebook([FromBody]SocialAuthViewModel model) { + // 1.generate an app access token + var appAccessTokenResponse = await Client.GetStringAsync($"https://graph.facebook.com/oauth/access_token?client_id={_fbAuthSettings.AppId}&client_secret={_fbAuthSettings.AppSecret}&grant_type=client_credentials"); + var appAccessToken = JsonConvert.DeserializeObject(appAccessTokenResponse); + // 2. validate the user access token + var userAccessTokenValidationResponse = await Client.GetStringAsync($"https://graph.facebook.com/debug_token?input_token={model.AccessToken}&access_token={appAccessToken.AccessToken}"); + var userAccessTokenValidation = JsonConvert.DeserializeObject(userAccessTokenValidationResponse); + + if (!userAccessTokenValidation.Data.IsValid) { + return BadRequest(Errors.AddErrorToModelState("login_failure", "Invalid facebook token.", ModelState)); + } + + // 3. we've got a valid token so we can request user data from fb + var userInfoResponse = await Client.GetStringAsync($"https://graph.facebook.com/v2.8/me?fields=id,email,first_name,last_name,name,gender,locale,birthday,picture&access_token={model.AccessToken}"); + var userInfo = JsonConvert.DeserializeObject(userInfoResponse); + + return await _processUserDetails(userInfo); + } + private async Task _processUserDetails(FacebookUserData userInfo) { + // 4. ready to create the local user account (if necessary) and jwt + var user = await _userManager.FindByEmailAsync(userInfo.Email); + if (user == null) { + var appUser = new ApplicationUser { + FirstName = userInfo.FirstName, + LastName = userInfo.LastName, + FacebookId = userInfo.Id, + Email = userInfo.Email, + UserName = userInfo.Email, + PictureUrl = userInfo.Picture.Data.Url + }; + var result = await _userManager.CreateAsync(appUser, Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Substring(0, 8)); + if (!result.Succeeded) return new BadRequestObjectResult(Errors.AddErrorsToModelState(result, ModelState)); + } else { + user.PictureUrl = userInfo.Picture.Data.Url; + var result = await _userManager.UpdateAsync(user); + if (!result.Succeeded) return new BadRequestObjectResult(Errors.AddErrorsToModelState(result, ModelState)); + } + + // generate the jwt for the local user... + var localUser = await _userManager.FindByNameAsync(userInfo.Email); + + if (localUser == null) { + return BadRequest(Errors.AddErrorToModelState("login_failure", "Failed to create local user account.", ModelState)); + } + + var jwt = await Tokens.GenerateJwt(_jwtFactory.GenerateClaimsIdentity(localUser.UserName, localUser.Id), + _jwtFactory, localUser.UserName, _jwtOptions, new JsonSerializerSettings { Formatting = Formatting.Indented }); + + return new OkObjectResult(jwt); + } + } +} diff --git a/server/Models/FacebookApiResponses.cs b/server/Models/FacebookApiResponses.cs new file mode 100644 index 0000000..a5e1fec --- /dev/null +++ b/server/Models/FacebookApiResponses.cs @@ -0,0 +1,52 @@ +using Newtonsoft.Json; + +namespace PodNoms.Api.Models { + internal class FacebookUserData { + public long Id { get; set; } + public string Email { get; set; } + public string Name { get; set; } + [JsonProperty("first_name")] + public string FirstName { get; set; } + [JsonProperty("last_name")] + public string LastName { get; set; } + public string Gender { get; set; } + public string Locale { get; set; } + public FacebookPictureData Picture { get; set; } + } + + internal class FacebookPictureData { + public FacebookPicture Data { get; set; } + } + + internal class FacebookPicture { + public int Height { get; set; } + public int Width { get; set; } + [JsonProperty("is_silhouette")] + public bool IsSilhouette { get; set; } + public string Url { get; set; } + } + + internal class FacebookUserAccessTokenData { + [JsonProperty("app_id")] + public long AppId { get; set; } + public string Type { get; set; } + public string Application { get; set; } + [JsonProperty("expires_at")] + public long ExpiresAt { get; set; } + [JsonProperty("is_valid")] + public bool IsValid { get; set; } + [JsonProperty("user_id")] + public long UserId { get; set; } + } + + internal class FacebookUserAccessTokenValidation { + public FacebookUserAccessTokenData Data { get; set; } + } + + internal class FacebookAppAccessToken { + [JsonProperty("token_type")] + public string TokenType { get; set; } + [JsonProperty("access_token")] + public string AccessToken { get; set; } + } +} diff --git a/server/Models/FacebookAuthSettings.cs b/server/Models/FacebookAuthSettings.cs new file mode 100644 index 0000000..3978c3b --- /dev/null +++ b/server/Models/FacebookAuthSettings.cs @@ -0,0 +1,6 @@ +namespace PodNoms.Api.Models { + public class FacebookAuthSettings { + public string AppId { get; set; } + public string AppSecret { get; set; } + } +} \ No newline at end of file diff --git a/server/Models/ViewModels/SocialAuthViewModel.cs b/server/Models/ViewModels/SocialAuthViewModel.cs new file mode 100644 index 0000000..2cd2bf7 --- /dev/null +++ b/server/Models/ViewModels/SocialAuthViewModel.cs @@ -0,0 +1,6 @@ +namespace PodNoms.Api.Models.ViewModels { + public class SocialAuthViewModel { + public string AccessToken { get; set; } + } + +} \ No newline at end of file