From 1c0996c625dd7f8212d3fa4893dd7f6529a924f9 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 15 Dec 2015 13:49:32 -0800 Subject: [PATCH] Add a sample demonstrating Antiforgery with AJAX --- .../FormPostSampleMiddleware.cs | 66 ------------------- samples/AntiforgerySample/Startup.cs | 57 +++++++++++++++- samples/AntiforgerySample/TodoItem.cs | 13 ++++ samples/AntiforgerySample/TodoRepository.cs | 31 +++++++++ samples/AntiforgerySample/bower.json | 8 +++ samples/AntiforgerySample/gulpfile.js | 18 +++++ samples/AntiforgerySample/package.json | 13 ++++ samples/AntiforgerySample/project.json | 20 +++--- samples/AntiforgerySample/wwwroot/Index.html | 45 +++++++++++-- samples/AntiforgerySample/wwwroot/app.js | 4 ++ .../AntiforgerySample/wwwroot/controllers.js | 21 ++++++ samples/AntiforgerySample/wwwroot/favicon.ico | 1 - samples/AntiforgerySample/wwwroot/services.js | 22 +++++++ .../ServiceCollectionExtensions.cs | 9 +-- 14 files changed, 241 insertions(+), 87 deletions(-) delete mode 100644 samples/AntiforgerySample/FormPostSampleMiddleware.cs create mode 100644 samples/AntiforgerySample/TodoItem.cs create mode 100644 samples/AntiforgerySample/TodoRepository.cs create mode 100644 samples/AntiforgerySample/bower.json create mode 100644 samples/AntiforgerySample/gulpfile.js create mode 100644 samples/AntiforgerySample/package.json create mode 100644 samples/AntiforgerySample/wwwroot/app.js create mode 100644 samples/AntiforgerySample/wwwroot/controllers.js delete mode 100644 samples/AntiforgerySample/wwwroot/favicon.ico create mode 100644 samples/AntiforgerySample/wwwroot/services.js diff --git a/samples/AntiforgerySample/FormPostSampleMiddleware.cs b/samples/AntiforgerySample/FormPostSampleMiddleware.cs deleted file mode 100644 index ef0a8a5421..0000000000 --- a/samples/AntiforgerySample/FormPostSampleMiddleware.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; -using Microsoft.AspNet.Antiforgery; -using Microsoft.AspNet.Builder; -using Microsoft.AspNet.Http; -using Microsoft.Extensions.OptionsModel; - -namespace AntiforgerySample -{ - public class FormPostSampleMiddleware - { - private readonly IAntiforgery _antiforgery; - private readonly AntiforgeryOptions _options; - private readonly RequestDelegate _next; - - public FormPostSampleMiddleware( - RequestDelegate next, - IAntiforgery antiforgery, - IOptions options) - { - _next = next; - _antiforgery = antiforgery; - _options = options.Value; - } - - public async Task Invoke(HttpContext context) - { - if (context.Request.Method == "GET") - { - var page = -@" - -
- - -
- -"; - - var tokenSet = _antiforgery.GetAndStoreTokens(context); - await context.Response.WriteAsync(string.Format(page, _options.FormFieldName, tokenSet.RequestToken)); - } - else if (context.Request.Method == "POST") - { - // This will throw if invalid. - await _antiforgery.ValidateRequestAsync(context); - - var page = -@" - -

Everything is fine

-

Try Again

- - -"; - await context.Response.WriteAsync(page); - } - else - { - await _next(context); - } - } - } -} diff --git a/samples/AntiforgerySample/Startup.cs b/samples/AntiforgerySample/Startup.cs index 6fef43186c..7920eb3c6d 100644 --- a/samples/AntiforgerySample/Startup.cs +++ b/samples/AntiforgerySample/Startup.cs @@ -1,8 +1,15 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.IO; +using Microsoft.AspNet.Antiforgery; using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.OptionsModel; +using Newtonsoft.Json; namespace AntiforgerySample { @@ -10,13 +17,57 @@ namespace AntiforgerySample { public void ConfigureServices(IServiceCollection services) { - services.AddAntiforgery(); + services.AddRouting(); + + // Angular's default header name for sending the XSRF token. + services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN"); + + services.AddSingleton(); } - public void Configure(IApplicationBuilder app) + public void Configure(IApplicationBuilder app, IAntiforgery antiforgery, IOptions options, TodoRepository repository) { + app.Use(next => context => + { + if ( + string.Equals(context.Request.Path.Value, "/", StringComparison.OrdinalIgnoreCase) || + string.Equals(context.Request.Path.Value, "/index.html", StringComparison.OrdinalIgnoreCase)) + { + // We can send the request token as a JavaScript-readable cookie, and Angular will use it by default. + var tokens = antiforgery.GetAndStoreTokens(context); + context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false }); + } + + return next(context); + }); + + app.UseDefaultFiles(); app.UseStaticFiles(); - app.UseMiddleware(); + + var routes = new RouteBuilder(app); + + routes.MapGet("api/items", (HttpContext context) => + { + var items = repository.GetItems(); + return context.Response.WriteAsync(JsonConvert.SerializeObject(items)); + }); + + routes.MapPost("api/items", async (HttpContext context) => + { + // This will throw if the token is invalid. + await antiforgery.ValidateRequestAsync(context); + + var serializer = new JsonSerializer(); + using (var reader = new JsonTextReader(new StreamReader(context.Request.Body))) + { + var item = serializer.Deserialize(reader); + repository.Add(item); + } + + context.Response.StatusCode = 204; + }); + + app.UseRouter(routes.Build()); } } } diff --git a/samples/AntiforgerySample/TodoItem.cs b/samples/AntiforgerySample/TodoItem.cs new file mode 100644 index 0000000000..e58adb37ad --- /dev/null +++ b/samples/AntiforgerySample/TodoItem.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Newtonsoft.Json; + +namespace AntiforgerySample +{ + public class TodoItem + { + [JsonProperty(PropertyName = "name")] + public string Name { get; set; } + } +} diff --git a/samples/AntiforgerySample/TodoRepository.cs b/samples/AntiforgerySample/TodoRepository.cs new file mode 100644 index 0000000000..8625882c0a --- /dev/null +++ b/samples/AntiforgerySample/TodoRepository.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace AntiforgerySample +{ + public class TodoRepository + { + private List _items; + + public TodoRepository() + { + _items = new List() + { + new TodoItem() { Name = "Mow the lawn" }, + new TodoItem() { Name = "Do the dishes" }, + }; + } + + public IEnumerable GetItems() + { + return _items; + } + + public void Add(TodoItem item) + { + _items.Add(item); + } + } +} diff --git a/samples/AntiforgerySample/bower.json b/samples/AntiforgerySample/bower.json new file mode 100644 index 0000000000..89471a5f27 --- /dev/null +++ b/samples/AntiforgerySample/bower.json @@ -0,0 +1,8 @@ +{ + "name": "ASP.NET", + "private": true, + "dependencies": { + "angular": "~1.4.8", + "bootstrap-css": "~3.3.4" + } +} diff --git a/samples/AntiforgerySample/gulpfile.js b/samples/AntiforgerySample/gulpfile.js new file mode 100644 index 0000000000..dedfea6f45 --- /dev/null +++ b/samples/AntiforgerySample/gulpfile.js @@ -0,0 +1,18 @@ +/// +"use strict"; + +var gulp = require("gulp"), + bowerFiles = require('main-bower-files'); + +var paths = { + webroot: "./wwwroot/" +}; + +paths.bowerFilesDest = paths.webroot + '/bower_components'; + +gulp.task("copy:bower", function () { + return gulp.src(bowerFiles()).pipe(gulp.dest(paths.bowerFilesDest)); +}); + +gulp.task("default", ["copy:bower"]); + diff --git a/samples/AntiforgerySample/package.json b/samples/AntiforgerySample/package.json new file mode 100644 index 0000000000..e3693fbdb7 --- /dev/null +++ b/samples/AntiforgerySample/package.json @@ -0,0 +1,13 @@ +{ + "name": "ASP.NET", + "private": true, + "version": "0.0.0", + "devDependencies": { + "gulp": "^3.9.0", + "gulp-concat": "^2.6.0", + "gulp-cssmin": "^0.1.7", + "gulp-uglify": "^1.5.1", + "main-bower-files": "^2.9.0", + "rimraf": "^2.4.4" + } +} diff --git a/samples/AntiforgerySample/project.json b/samples/AntiforgerySample/project.json index 34f061b15b..0bd3d43bb3 100644 --- a/samples/AntiforgerySample/project.json +++ b/samples/AntiforgerySample/project.json @@ -1,15 +1,17 @@ -{ +{ "webroot": "wwwroot", "version": "1.0.0-*", - "dependencies": { - "Microsoft.AspNet.Antiforgery": "1.0.0-*", - "Microsoft.AspNet.Http.Extensions": "1.0.0-*", - "Microsoft.AspNet.Server.IIS": "1.0.0-*", - "Microsoft.AspNet.Server.Kestrel": "1.0.0-*", - "Microsoft.AspNet.Server.WebListener": "1.0.0-*", - "Microsoft.AspNet.StaticFiles": "1.0.0-*" - }, + "dependencies": { + "Microsoft.AspNet.Antiforgery": "1.0.0-*", + "Microsoft.AspNet.Http.Abstractions": "1.0.0-rc2-16062", + "Microsoft.AspNet.Http.Extensions": "1.0.0-*", + "Microsoft.AspNet.Routing.Extensions": "1.0.0-*", + "Microsoft.AspNet.Server.Kestrel": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.AspNet.StaticFiles": "1.0.0-*", + "Newtonsoft.Json": "7.0.1" + }, "commands": { "kestrel": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.Kestrel --server.urls http://localhost:5000", diff --git a/samples/AntiforgerySample/wwwroot/Index.html b/samples/AntiforgerySample/wwwroot/Index.html index 94edeefc78..b9a1fc0182 100644 --- a/samples/AntiforgerySample/wwwroot/Index.html +++ b/samples/AntiforgerySample/wwwroot/Index.html @@ -1,10 +1,47 @@  - + + - Antiforgery Sample + Todo List Antiforgery Sample + - -

Hello, World!

+ +
+
+

Todo List Antiforgery Sample

+
+
+
+ + + + + + + + + + +
TODO List
{{$index + 1}} + {{item.name}} +
+
+
+
+
+
+ + +
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/samples/AntiforgerySample/wwwroot/app.js b/samples/AntiforgerySample/wwwroot/app.js new file mode 100644 index 0000000000..f57425a553 --- /dev/null +++ b/samples/AntiforgerySample/wwwroot/app.js @@ -0,0 +1,4 @@ +angular.module('TODO', [ + 'TODO.controllers', + 'TODO.services' +]); \ No newline at end of file diff --git a/samples/AntiforgerySample/wwwroot/controllers.js b/samples/AntiforgerySample/wwwroot/controllers.js new file mode 100644 index 0000000000..df35c5cab0 --- /dev/null +++ b/samples/AntiforgerySample/wwwroot/controllers.js @@ -0,0 +1,21 @@ +angular.module('TODO.controllers', []). +controller('todoController', function ($scope, todoApi) { + $scope.itemList = []; + $scope.item = {}; + + $scope.refresh = function (item) { + todoApi.getItems().success(function (response) { + $scope.itemList = response; + }); + }; + + $scope.create = function (item) { + todoApi.create(item).success(function (response) { + $scope.item = {}; + $scope.refresh(); + }); + }; + + // Load initial items + $scope.refresh(); +}); \ No newline at end of file diff --git a/samples/AntiforgerySample/wwwroot/favicon.ico b/samples/AntiforgerySample/wwwroot/favicon.ico deleted file mode 100644 index 5f282702bb..0000000000 --- a/samples/AntiforgerySample/wwwroot/favicon.ico +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/samples/AntiforgerySample/wwwroot/services.js b/samples/AntiforgerySample/wwwroot/services.js new file mode 100644 index 0000000000..be27281ba6 --- /dev/null +++ b/samples/AntiforgerySample/wwwroot/services.js @@ -0,0 +1,22 @@ +angular.module('TODO.services', []). + factory('todoApi', function ($http) { + + var todoApi = {}; + + todoApi.getItems = function () { + return $http({ + method: 'GET', + url: '/api/items' + }); + } + + todoApi.create = function (item) { + return $http({ + method: 'POST', + url: '/api/items', + data: item + }); + }; + + return todoApi; + }); \ No newline at end of file diff --git a/src/Microsoft.AspNet.Antiforgery/ServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Antiforgery/ServiceCollectionExtensions.cs index f728cdb40f..fc4839eb03 100644 --- a/src/Microsoft.AspNet.Antiforgery/ServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Antiforgery/ServiceCollectionExtensions.cs @@ -33,7 +33,7 @@ namespace Microsoft.Extensions.DependencyInjection return services; } - public static IServiceCollection ConfigureAntiforgery( + public static IServiceCollection AddAntiforgery( this IServiceCollection services, Action setupAction) { @@ -42,12 +42,13 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(services)); } - if (setupAction == null) + services.AddAntiforgery(); + + if (setupAction != null) { - throw new ArgumentNullException(nameof(setupAction)); + services.Configure(setupAction); } - services.Configure(setupAction); return services; } }