From 58de9d9a4ec71729fbb78678a8fed87f4080c11a Mon Sep 17 00:00:00 2001 From: Jonas Dellinger Date: Tue, 19 Nov 2019 10:37:42 +0100 Subject: [PATCH] Refactor ASP.NET Example to use AspNet.Security.OAuth.Spotify --- .../ClaimsExtensions.cs | 15 --- .../Controllers/AuthController.cs | 93 ------------------- .../Controllers/HomeController.cs | 64 ++++++------- .../Models/ErrorViewModel.cs | 4 +- .../Models/HomeModel.cs | 9 -- .../Models/IndexModel.cs | 10 ++ SpotifyAPI.Web.Examples.ASP/Program.cs | 18 ++-- .../Properties/launchSettings.json | 12 +-- .../SpotifyAPI.Web.Examples.ASP.csproj | 8 +- SpotifyAPI.Web.Examples.ASP/Startup.cs | 63 ++++++------- .../Views/Home/Index.cshtml | 48 +++------- .../Views/Home/Privacy.cshtml | 6 ++ .../Views/Shared/Error.cshtml | 1 - .../Views/Shared/_Layout.cshtml | 50 ++-------- .../Shared/_ValidationScriptsPartial.cshtml | 20 +--- SpotifyAPI.Web.Examples.ASP/appsettings.json | 4 +- .../wwwroot/css/site.css | 19 +++- .../lib/bootstrap/dist/css/bootstrap.css | 5 +- 18 files changed, 140 insertions(+), 309 deletions(-) delete mode 100644 SpotifyAPI.Web.Examples.ASP/ClaimsExtensions.cs delete mode 100644 SpotifyAPI.Web.Examples.ASP/Controllers/AuthController.cs delete mode 100644 SpotifyAPI.Web.Examples.ASP/Models/HomeModel.cs create mode 100644 SpotifyAPI.Web.Examples.ASP/Models/IndexModel.cs create mode 100644 SpotifyAPI.Web.Examples.ASP/Views/Home/Privacy.cshtml diff --git a/SpotifyAPI.Web.Examples.ASP/ClaimsExtensions.cs b/SpotifyAPI.Web.Examples.ASP/ClaimsExtensions.cs deleted file mode 100644 index 5807579c..00000000 --- a/SpotifyAPI.Web.Examples.ASP/ClaimsExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Linq; -using System.Security.Claims; - -namespace SpotifyAPI.Web.Examples.ASP -{ - public static class ClaimsExtensions - { - public static string GetSpecificClaim(this ClaimsIdentity claimsIdentity, string claimType) - { - Claim claim = claimsIdentity.Claims.FirstOrDefault(x => x.Type == claimType); - - return claim != null ? claim.Value : string.Empty; - } - } -} \ No newline at end of file diff --git a/SpotifyAPI.Web.Examples.ASP/Controllers/AuthController.cs b/SpotifyAPI.Web.Examples.ASP/Controllers/AuthController.cs deleted file mode 100644 index aebe104e..00000000 --- a/SpotifyAPI.Web.Examples.ASP/Controllers/AuthController.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Mvc; -using SpotifyAPI.Web.Auth; -using SpotifyAPI.Web.Enums; -using SpotifyAPI.Web.Examples.ASP.Models; -using SpotifyAPI.Web.Models; -using Microsoft.Extensions.Configuration; - -namespace SpotifyAPI.Web.Examples.ASP.Controllers -{ - public class Auth : Controller - { - public IConfiguration Configuration { get; set; } - - public Auth(IConfiguration config) - { - Configuration = config; - } - - private static Dictionary LoginRequests { get; } = new Dictionary(); - - // GET - public IActionResult Login() - { - string guid = Guid.NewGuid().ToString(); - var auth = new AuthorizationCodeAuth( // TODO: Extract to own method - Configuration["spotify_client_id"], Configuration["spotify_client_secret"], - "http://localhost:5000/auth/callback", "", Scope.PlaylistReadPrivate | Scope.PlaylistReadPrivate, guid); - - LoginRequests.Add(guid, auth); // TODO: Clean up after a timeout, else: DOS possible - - return Redirect(auth.GetUri()); - } - - [HttpPost] - public async Task Logout() - { - await HttpContext.SignOutAsync(); - - return RedirectToAction("Index", "Home"); - } - - public async Task Callback([FromQuery(Name = "code")] string code, [FromQuery(Name = "state")] string state, - [FromQuery(Name = "error")] string error) - { - if (error != null) - return View("Error", new ErrorViewModel { Message = error, RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier}); - - if(!LoginRequests.ContainsKey(state)) - return View("Error", new ErrorViewModel { Message = "Unknown login request", RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier}); - - AuthorizationCodeAuth auth = LoginRequests[state]; - Token token = await auth.ExchangeCode(code); - - if (token.HasError()) - { - return View("Error", - new ErrorViewModel - { - Message = $"Unable to exchange Code: ${token.Error}", - RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier - }); - } - - var api = new SpotifyWebAPI - { - AccessToken = token.AccessToken, - TokenType = token.TokenType - }; - PrivateProfile profile = await api.GetPrivateProfileAsync(); - - var claims = new List - { - // TODO: Extract claim types to either Enum or class - new Claim("user_id", profile.Id), - new Claim("display_name", profile.DisplayName ?? profile.Id), - new Claim("access_token", token.AccessToken), - new Claim("token_type", token.TokenType), - new Claim("refresh_token", token.RefreshToken), - new Claim("expires_in", token.ExpiresIn.ToString(CultureInfo.InvariantCulture)) - }; - await HttpContext.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies", "user_id", ""))); - - return Redirect("/"); - } - } -} \ No newline at end of file diff --git a/SpotifyAPI.Web.Examples.ASP/Controllers/HomeController.cs b/SpotifyAPI.Web.Examples.ASP/Controllers/HomeController.cs index 29e104ff..759a080f 100644 --- a/SpotifyAPI.Web.Examples.ASP/Controllers/HomeController.cs +++ b/SpotifyAPI.Web.Examples.ASP/Controllers/HomeController.cs @@ -1,54 +1,48 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Security.Claims; +using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using SpotifyAPI.Web.Auth; -using SpotifyAPI.Web.Enums; +using Microsoft.Extensions.Logging; using SpotifyAPI.Web.Examples.ASP.Models; namespace SpotifyAPI.Web.Examples.ASP.Controllers { - [Authorize] public class HomeController : Controller { - public async Task Index() - { - var claimsIdent = User.Identity as ClaimsIdentity; + private readonly ILogger _logger; - SpotifyWebAPI api = GetSpotifyApiFromUser(claimsIdent); - if (api == null) - return View(new HomeModel()); - - string userId = claimsIdent.GetSpecificClaim("user_id"); - var playlists = await api.GetUserPlaylistsAsync(userId); - return View(new HomeModel + public HomeController(ILogger logger) + { + _logger = logger; + } + + public async Task Index() + { + if(!User.Identity.IsAuthenticated) + return Challenge(new AuthenticationProperties { RedirectUri = "/" }, "Spotify"); + + var accessToken = await HttpContext.GetTokenAsync("Spotify", "access_token"); + SpotifyWebAPI api = new SpotifyWebAPI { - Playlists = playlists - }); + AccessToken = accessToken, + TokenType = "Bearer" + }; + + var savedTracks = await api.GetSavedTracksAsync(50); + + return View(new IndexModel { SavedTracks = savedTracks }); + } + + public IActionResult Privacy() + { + return View(); } [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { - return View(new ErrorViewModel {RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier}); - } - - private static SpotifyWebAPI GetSpotifyApiFromUser(ClaimsIdentity claimsIdent) - { - // TODO: Add expires_in logic - - string accessToken = claimsIdent.GetSpecificClaim("access_token"); - string tokenType = claimsIdent.GetSpecificClaim("token_type"); - return new SpotifyWebAPI - { - AccessToken = accessToken, - TokenType = tokenType - }; + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } } -} \ No newline at end of file +} diff --git a/SpotifyAPI.Web.Examples.ASP/Models/ErrorViewModel.cs b/SpotifyAPI.Web.Examples.ASP/Models/ErrorViewModel.cs index ce8265f2..e7e3c079 100644 --- a/SpotifyAPI.Web.Examples.ASP/Models/ErrorViewModel.cs +++ b/SpotifyAPI.Web.Examples.ASP/Models/ErrorViewModel.cs @@ -7,7 +7,5 @@ namespace SpotifyAPI.Web.Examples.ASP.Models public string RequestId { get; set; } public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - - public string Message { get; set; } } -} \ No newline at end of file +} diff --git a/SpotifyAPI.Web.Examples.ASP/Models/HomeModel.cs b/SpotifyAPI.Web.Examples.ASP/Models/HomeModel.cs deleted file mode 100644 index 6c7e01d5..00000000 --- a/SpotifyAPI.Web.Examples.ASP/Models/HomeModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -using SpotifyAPI.Web.Models; - -namespace SpotifyAPI.Web.Examples.ASP.Models -{ - public class HomeModel - { - public Paging Playlists; - } -} \ No newline at end of file diff --git a/SpotifyAPI.Web.Examples.ASP/Models/IndexModel.cs b/SpotifyAPI.Web.Examples.ASP/Models/IndexModel.cs new file mode 100644 index 00000000..149e1497 --- /dev/null +++ b/SpotifyAPI.Web.Examples.ASP/Models/IndexModel.cs @@ -0,0 +1,10 @@ +using System; +using SpotifyAPI.Web.Models; + +namespace SpotifyAPI.Web.Examples.ASP.Models +{ + public class IndexModel + { + public Paging SavedTracks; + } +} diff --git a/SpotifyAPI.Web.Examples.ASP/Program.cs b/SpotifyAPI.Web.Examples.ASP/Program.cs index 7bafae58..a8acd307 100644 --- a/SpotifyAPI.Web.Examples.ASP/Program.cs +++ b/SpotifyAPI.Web.Examples.ASP/Program.cs @@ -1,11 +1,10 @@ -using System; +using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; -using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace SpotifyAPI.Web.Examples.ASP @@ -14,11 +13,14 @@ namespace SpotifyAPI.Web.Examples.ASP { public static void Main(string[] args) { - CreateWebHostBuilder(args).Build().Run(); + CreateHostBuilder(args).Build().Run(); } - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup(); + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); } -} \ No newline at end of file +} diff --git a/SpotifyAPI.Web.Examples.ASP/Properties/launchSettings.json b/SpotifyAPI.Web.Examples.ASP/Properties/launchSettings.json index 9619c997..bf29a4fb 100644 --- a/SpotifyAPI.Web.Examples.ASP/Properties/launchSettings.json +++ b/SpotifyAPI.Web.Examples.ASP/Properties/launchSettings.json @@ -1,10 +1,10 @@ { "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, + "windowsAuthentication": false, + "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:7731", - "sslPort": 44310 + "applicationUrl": "http://localhost:55802", + "sslPort": 44320 } }, "profiles": { @@ -18,10 +18,10 @@ "SpotifyAPI.Web.Examples.ASP": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "https://localhost:5001;http://localhost:5000", + "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } -} \ No newline at end of file +} diff --git a/SpotifyAPI.Web.Examples.ASP/SpotifyAPI.Web.Examples.ASP.csproj b/SpotifyAPI.Web.Examples.ASP/SpotifyAPI.Web.Examples.ASP.csproj index cf1eb3a6..bfd35db9 100644 --- a/SpotifyAPI.Web.Examples.ASP/SpotifyAPI.Web.Examples.ASP.csproj +++ b/SpotifyAPI.Web.Examples.ASP/SpotifyAPI.Web.Examples.ASP.csproj @@ -2,10 +2,12 @@ netcoreapp3.0 - InProcess - 52b27bb1-dc8f-4151-8ad6-b6efdb8edbb7 + 201e6943-a5b3-473c-b1da-a579b36ca283 - + + + + \ No newline at end of file diff --git a/SpotifyAPI.Web.Examples.ASP/Startup.cs b/SpotifyAPI.Web.Examples.ASP/Startup.cs index 66e1e913..56618677 100644 --- a/SpotifyAPI.Web.Examples.ASP/Startup.cs +++ b/SpotifyAPI.Web.Examples.ASP/Startup.cs @@ -1,23 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using EnvironmentName = Microsoft.AspNetCore.Hosting.EnvironmentName; +using SpotifyAPI.Web.Enums; namespace SpotifyAPI.Web.Examples.ASP { public class Startup { - private const string CookieScheme = "YourSchemeName"; - public Startup(IConfiguration configuration) { Configuration = configuration; @@ -28,25 +20,24 @@ namespace SpotifyAPI.Web.Examples.ASP // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.Configure(options => - { - // This lambda determines whether user consent for non-essential cookies is needed for a given request. - options.CheckConsentNeeded = context => true; - options.MinimumSameSitePolicy = SameSiteMode.None; - }); - - services - .AddAuthentication(CookieScheme) // Sets the default scheme to cookies - .AddCookie(CookieScheme, options => + services.AddControllersWithViews(); + + services.AddAuthentication(o => o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(options => { - options.LoginPath = "/auth/login"; + options.LoginPath = "/signin"; + options.LogoutPath = "/signout"; + }) + .AddSpotify(options => + { + var scopes = Scope.UserLibraryRead; + options.Scope.Add(scopes.GetStringAttribute(",")); + + options.SaveTokens = true; + options.ClientId = Configuration["client_id"]; + options.ClientSecret = Configuration["client_secret"]; + options.CallbackPath = "/callback"; }); - - - - services - .AddMvc(options => options.EnableEndpointRouting = false) - .SetCompatibilityVersion(CompatibilityVersion.Version_3_0); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -58,24 +49,24 @@ namespace SpotifyAPI.Web.Examples.ASP } else { - app.UseHttpsRedirection(); app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); - app.UseHttpsRedirection(); } - - + // app.UseHttpsRedirection(); app.UseStaticFiles(); - app.UseCookiePolicy(); + + app.UseRouting(); app.UseAuthentication(); - app.UseMvc(routes => + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { - routes.MapRoute( + endpoints.MapControllerRoute( name: "default", - template: "{controller=Home}/{action=Index}/{id?}"); + pattern: "{controller=Home}/{action=Index}/{id?}"); }); } } -} \ No newline at end of file +} diff --git a/SpotifyAPI.Web.Examples.ASP/Views/Home/Index.cshtml b/SpotifyAPI.Web.Examples.ASP/Views/Home/Index.cshtml index 48cfbcfe..c1b077fc 100644 --- a/SpotifyAPI.Web.Examples.ASP/Views/Home/Index.cshtml +++ b/SpotifyAPI.Web.Examples.ASP/Views/Home/Index.cshtml @@ -1,39 +1,15 @@ - @model HomeModel - @using System.Security.Claims - @using Microsoft.AspNetCore.Authentication - @{ +@model IndexModel +@{ ViewData["Title"] = "Home Page"; - } - -

Some Playlists (first 20)

- - @if (Model.Playlists != null && !Model.Playlists.HasError()) - { -
- @foreach (var playlist in Model.Playlists.Items) - { -
@playlist.Name
-
@(playlist.Owner.DisplayName ?? playlist.Owner.Id)
- } -
- } +} -

HttpContext.User.Claims

+
+ You have @Model.SavedTracks.Total saved tracks in your library! Here are 50 of them: -
- @foreach (Claim claim in User.Claims) - { -
@claim.Type
-
@claim.Value
- } -
- -

AuthenticationProperties

- -
- @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items) - { -
@prop.Key
-
@prop.Value
- } -
\ No newline at end of file +
    + @foreach (var item in Model.SavedTracks.Items) + { +
  • @item.Track.Name
  • + } +
+
diff --git a/SpotifyAPI.Web.Examples.ASP/Views/Home/Privacy.cshtml b/SpotifyAPI.Web.Examples.ASP/Views/Home/Privacy.cshtml new file mode 100644 index 00000000..af4fb195 --- /dev/null +++ b/SpotifyAPI.Web.Examples.ASP/Views/Home/Privacy.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/SpotifyAPI.Web.Examples.ASP/Views/Shared/Error.cshtml b/SpotifyAPI.Web.Examples.ASP/Views/Shared/Error.cshtml index fe2feffd..a1e04783 100644 --- a/SpotifyAPI.Web.Examples.ASP/Views/Shared/Error.cshtml +++ b/SpotifyAPI.Web.Examples.ASP/Views/Shared/Error.cshtml @@ -9,7 +9,6 @@ @if (Model.ShowRequestId) {

- Message: @Model.Message Request ID: @Model.RequestId

} diff --git a/SpotifyAPI.Web.Examples.ASP/Views/Shared/_Layout.cshtml b/SpotifyAPI.Web.Examples.ASP/Views/Shared/_Layout.cshtml index 5ff3d596..63a1efaf 100644 --- a/SpotifyAPI.Web.Examples.ASP/Views/Shared/_Layout.cshtml +++ b/SpotifyAPI.Web.Examples.ASP/Views/Shared/_Layout.cshtml @@ -1,20 +1,10 @@  - + @ViewData["Title"] - SpotifyAPI.Web.Examples.ASP - - - - - - - + @@ -31,14 +21,9 @@ - @if (User.Identity.IsAuthenticated) - { -
  • -
    - -
    -
  • - } + @@ -51,28 +36,13 @@
    +
    + © 2019 - SpotifyAPI.Web.Examples.ASP - Privacy +
    - - - - - - - - - + + - @RenderSection("Scripts", required: false) diff --git a/SpotifyAPI.Web.Examples.ASP/Views/Shared/_ValidationScriptsPartial.cshtml b/SpotifyAPI.Web.Examples.ASP/Views/Shared/_ValidationScriptsPartial.cshtml index 3c0e0777..5a16d80a 100644 --- a/SpotifyAPI.Web.Examples.ASP/Views/Shared/_ValidationScriptsPartial.cshtml +++ b/SpotifyAPI.Web.Examples.ASP/Views/Shared/_ValidationScriptsPartial.cshtml @@ -1,18 +1,2 @@ - - - - - - - - + + diff --git a/SpotifyAPI.Web.Examples.ASP/appsettings.json b/SpotifyAPI.Web.Examples.ASP/appsettings.json index def9159a..d9d9a9bf 100644 --- a/SpotifyAPI.Web.Examples.ASP/appsettings.json +++ b/SpotifyAPI.Web.Examples.ASP/appsettings.json @@ -1,7 +1,9 @@ { "Logging": { "LogLevel": { - "Default": "Warning" + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" diff --git a/SpotifyAPI.Web.Examples.ASP/wwwroot/css/site.css b/SpotifyAPI.Web.Examples.ASP/wwwroot/css/site.css index c486131d..e679a8ea 100644 --- a/SpotifyAPI.Web.Examples.ASP/wwwroot/css/site.css +++ b/SpotifyAPI.Web.Examples.ASP/wwwroot/css/site.css @@ -7,6 +7,23 @@ a.navbar-brand { word-break: break-all; } +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + /* Sticky footer styles -------------------------------------------------- */ html { @@ -50,7 +67,5 @@ body { bottom: 0; width: 100%; white-space: nowrap; - /* Set the fixed height of the footer here */ - height: 60px; line-height: 60px; /* Vertically center the text there */ } diff --git a/SpotifyAPI.Web.Examples.ASP/wwwroot/lib/bootstrap/dist/css/bootstrap.css b/SpotifyAPI.Web.Examples.ASP/wwwroot/lib/bootstrap/dist/css/bootstrap.css index 3f1a4208..8f475892 100644 --- a/SpotifyAPI.Web.Examples.ASP/wwwroot/lib/bootstrap/dist/css/bootstrap.css +++ b/SpotifyAPI.Web.Examples.ASP/wwwroot/lib/bootstrap/dist/css/bootstrap.css @@ -138,14 +138,14 @@ small { sub, sup { + position: relative; font-size: 75%; line-height: 0; - position: relative; vertical-align: baseline; } sub { - bottom: -0.25em; + bottom: -.25em; } sup { @@ -305,7 +305,6 @@ fieldset { padding: 0; margin: 0; border: 0; - padding: 0; } legend {