{"id":4330,"date":"2020-11-16T08:05:31","date_gmt":"2020-11-16T15:05:31","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/odata\/?p=4330"},"modified":"2020-11-16T08:05:31","modified_gmt":"2020-11-16T15:05:31","slug":"introducing-the-odata-web-api-authorization-library","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/odata\/introducing-the-odata-web-api-authorization-library\/","title":{"rendered":"Introducing the OData Web API Authorization library"},"content":{"rendered":"<p>I would like to introduce the OData authorization library for Web API. Using the OData ModelBuilder, you can annotate your EDM model with permission restrictions that inform your API what permissions are required for which operations. These annotations are based on the <a href=\"https:\/\/github.com\/oasis-tcs\/odata-vocabularies\/blob\/master\/vocabularies\/Org.OData.Capabilities.V1.md\">OData Capabilities Vocabulary<\/a>. However, nothing enforces these permissions on your API. You would need to manually define authorization policies in your application to apply these permissions. With the new authorization library, these permissions can be applied automatically to your OData API endpoints with a bit of set-up code.<\/p>\n<p>In this tutorial, I am going to show you how to use the library to add authorization to a simple OData API.<\/p>\n<p>The library is currently available in beta version on NuGet as <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.AspNetCore.OData.Authorization\/0.1.0-beta\">Microsoft.AspNetCore.OData.Authorization<\/a>, it currently only supports OData WebApi 7.x applications based on AspNetCore 3.1 with endpoint routing.<\/p>\n<h2>Creating the Applicaton<\/h2>\n<ul>\n<li class=\"code-line\" data-line=\"15\">Create an\u00a0ASP.NET\u00a0Core 3.1 web application, using the API template. Let&#8217;s call the application\u00a0<strong>ODataAuthorizationDemo<\/strong><\/li>\n<li class=\"code-line\" data-line=\"16\">Install the following NuGet packages:\n<ul>\n<li class=\"code-line\" data-line=\"17\"><code>Microsoft.AspNetCore.OData<\/code> (7.5.x)<\/li>\n<li class=\"code-line\" data-line=\"18\"><code>Microsoft.EntityFrameworkCore<\/code>\u00a0(we&#8217;ll use EF Core for interacting with a database)<\/li>\n<li class=\"code-line\" data-line=\"19\"><code>Microsoft.EntityFrameworkCore.InMemory<\/code>\u00a0(we&#8217;ll use an in-memory database for this demo)<\/li>\n<li class=\"code-line\" data-line=\"20\"><code>Microsoft.OData.ModelBuilder<\/code>\u00a0(1.0.3 or later) (we&#8217;ll use to create the OData model and specify the permission restrictions)<\/li>\n<li class=\"code-line\" data-line=\"21\"><code>Microsoft.AspNetCore.OData.Authorization<\/code> (0.1.0-beta) (the WebApi Authorization library, see instructions below)<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>When installing the authorization package, go to <strong>Manage NuGet Packages<\/strong>\u00a0as you would normally when installing packages. Make sure to check the\u00a0<strong>Include prerelease<\/strong> checkbox. You should now be able to search for\u00a0<code>Microsoft.AspNetCore.OData.Authorization<\/code>.<\/p>\n<h3 id=\"create-the-db-context-and-model-classes\" class=\"code-line\" data-line=\"36\">Create the DB Context and model classes<\/h3>\n<p class=\"code-line\" data-line=\"38\">For demo purposes, we are only going to create one entity:\u00a0<code>Product<\/code>.<\/p>\n<p class=\"code-line\" data-line=\"40\">Create a folder called\u00a0<code>Models<\/code>. Add the following class file to that folder and call it\u00a0<code>Product.cs<\/code>:<\/p>\n<p>&nbsp;<\/p>\n<pre class=\"prettyprint\">using System.ComponentModel.DataAnnotations;\r\n\r\nnamespace ODataAuthorizationDemo.Models\r\n{\r\n    public class Product\r\n    {\r\n        public int Id { get; set; }\r\n        public string Name { get; set; }\r\n        public int Price { get; set; }\r\n    }\r\n}<\/pre>\n<p>&nbsp;<\/p>\n<p>Next, let&#8217;s create our EF Core database context. Add an\u00a0<code>AppDbContext.cs<\/code>\u00a0file to the\u00a0<code>Models<\/code>\u00a0folder with the following code:<\/p>\n<pre class=\"prettyprint\">using Microsoft.EntityFrameworkCore;\r\n\r\nnamespace ODataAuthorizationDemo.Models\r\n{\r\n    public class AppDbContext : DbContext\r\n    {\r\n        public AppDbContext(DbContextOptions&lt;AppDbContext&gt; options) : base(options)\r\n        {\r\n        }\r\n\r\n        public DbSet&lt;Product&gt; Products { get; set; }\r\n    }\r\n}<\/pre>\n<p>&nbsp;<\/p>\n<h3 id=\"create-the-odata-model\" class=\"code-line\" data-line=\"74\">Create the OData Model<\/h3>\n<p class=\"code-line\" data-line=\"76\">For this demo, we&#8217;ll use the OData ModelBuilder package to create an OData model based on our C# model classes. We&#8217;ll also use the model builder to add permission restrictions.<\/p>\n<p class=\"code-line\" data-line=\"78\">Let&#8217;s add an\u00a0<code>AppEdmModel.cs<\/code>\u00a0in the\u00a0<code>Models<\/code>\u00a0folder with the following code:<\/p>\n<pre class=\"prettyprint\">using Microsoft.OData.Edm;\r\nusing Microsoft.OData.ModelBuilder;\r\n\r\nnamespace ODataAuthorizationDemo.Models\r\n{\r\n    public static class AppEdmModel\r\n    {\r\n        public static IEdmModel GetModel()\r\n        {\r\n            var builder = new ODataConventionModelBuilder();\r\n            var products = builder.EntitySet&lt;Product&gt;(\"Products\");\r\n\r\n            products.HasReadRestrictions()\r\n                .HasPermissions(p =&gt;\r\n                    p.HasSchemeName(\"Scheme\").HasScopes(s =&gt; s.HasScope(\"Product.Read\")))\r\n                .HasReadByKeyRestrictions(r =&gt; r.HasPermissions(p =&gt;\r\n                    p.HasSchemeName(\"Scheme\").HasScopes(s =&gt; s.HasScope(\"Product.ReadByKey\"))));\r\n\r\n            products.HasInsertRestrictions()\r\n                .HasPermissions(p =&gt; p.HasSchemeName(\"Scheme\").HasScopes(s =&gt; s.HasScope(\"Product.Create\")));\r\n\r\n            products.HasUpdateRestrictions()\r\n                .HasPermissions(p =&gt; p.HasSchemeName(\"Scheme\").HasScopes(s =&gt; s.HasScope(\"Product.Update\")));\r\n\r\n            products.HasDeleteRestrictions()\r\n                .HasPermissions(p =&gt; p.HasSchemeName(\"Scheme\").HasScopes(s =&gt; s.HasScope(\"Product.Delete\")));\r\n\r\n            return builder.GetEdmModel();\r\n        }\r\n    }\r\n}<\/pre>\n<p class=\"code-line\" data-line=\"114\">This creates a model with a\u00a0<code>Products<\/code>\u00a0entity set based on the\u00a0<code>Product<\/code>\u00a0entity type. It adds permission restrictions for different CRUD operations on that entity set, specifying which scopes are required to execute those operations.<\/p>\n<ul>\n<li class=\"code-line\" data-line=\"116\">Reading products (<code>GET \/Products<\/code>) requires the user to have the scope\u00a0<code>Product.Read<\/code><\/li>\n<li class=\"code-line\" data-line=\"117\">Reading a single product by its key (<code>GET \/Products(1)<\/code>) can also be access with the scope\u00a0<code>Product.ReadByKey<\/code>\u00a0in case the user does not have the\u00a0<code>Products.Read<\/code>\u00a0scope.<\/li>\n<li class=\"code-line\" data-line=\"118\">Creating a new product (<code>POST \/Products(1)<\/code>) requires the scope\u00a0<code>Product.Create<\/code><\/li>\n<li class=\"code-line\" data-line=\"119\">Updating a product (<code>PATCH \/Products(1)<\/code>) requires\u00a0<code>Product.Update<\/code><\/li>\n<li class=\"code-line\" data-line=\"120\">Deleting a product (<code>DELETE \/Products(1)<\/code>\u00a0requires\u00a0<code>Product.Delete<\/code>)<\/li>\n<\/ul>\n<p class=\"code-line\" data-line=\"122\">These restrictions are added as\u00a0<a title=\"https:\/\/github.com\/oasis-tcs\/odata-vocabularies\/blob\/master\/vocabularies\/Org.OData.Capabilities.V1.md\" href=\"https:\/\/github.com\/oasis-tcs\/odata-vocabularies\/blob\/master\/vocabularies\/Org.OData.Capabilities.V1.md\" data-href=\"https:\/\/github.com\/oasis-tcs\/odata-vocabularies\/blob\/master\/vocabularies\/Org.OData.Capabilities.V1.md\">capability annotations<\/a>\u00a0to the OData model. The authorization middleware will read these annotations to extract permission scopes required for different requests. An operation for which no restrictions have been defined will be allowed by default.<\/p>\n<h3 id=\"configure-startup-services\" class=\"code-line\" data-line=\"125\">Configure Startup services<\/h3>\n<p class=\"code-line\" data-line=\"127\">Now let&#8217;s configure the different services and the app builder in the\u00a0<code>Startup.cs<\/code>\u00a0file.<\/p>\n<p class=\"code-line\" data-line=\"129\">Let&#8217;s modify the\u00a0<code>ConfgiureServices<\/code>\u00a0method so that it looks like this:<\/p>\n<pre class=\"prettyprint\">public void ConfigureServices(IServiceCollection services)\r\n{\r\n    services.AddDbContext&lt;AppDbContext&gt;(opt =&gt; opt.UseInMemoryDatabase(\"ODataAuthDemo\"));\r\n\r\n    services.AddOData();\r\n\r\n    services.AddRouting();\r\n}<\/pre>\n<p data-line=\"129\">And the\u00a0<code>Configure<\/code>\u00a0method:<\/p>\n<pre class=\"prettyprint\">public void Configure(IApplicationBuilder app, IWebHostEnvironment env)\r\n{\r\n    if (env.IsDevelopment())\r\n    {\r\n        app.UseDeveloperExceptionPage();\r\n    }\r\n\r\n    app.UseRouting();\r\n\r\n    app.UseEndpoints(endpoints =&gt;\r\n    {\r\n        endpoints.Expand().Filter().Count().OrderBy();\r\n        endpoints.MapODataRoute(\"odata\", \"odata\", AppEdmModel.GetModel());\r\n    });\r\n}<\/pre>\n<div>\n<div>You may need to add the following <code>using<\/code> statements:<\/div>\n<\/div>\n<div>\n<pre class=\"prettyprint\">using Microsoft.EntityFrameworkCore;\r\nusing Microsoft.AspNet.OData.Extensions;\r\nusing ODataAuthorizationDemo.Models;<\/pre>\n<p class=\"code-line\" data-line=\"170\">Now if we run the project and visit the\u00a0<code>GET \/odata\/$metadata<\/code>\u00a0endpoint in a tool like Postman, we should see the generated EDM model. The model should contain annotations for the different restriction types that we specified:\u00a0<code>ReadRestrictions<\/code>,\u00a0<code>InsertRestrictions<\/code>,\u00a0<code>UpdateRestrictions<\/code>\u00a0and\u00a0<code>DeleteRestrictions<\/code>. Each of these restriction annotations should a\u00a0<code>Permissions<\/code>\u00a0property with the scopes that we defined.<\/p>\n<p class=\"code-line\" data-line=\"172\">Here&#8217;s an excerpt of the generated annotations:<\/p>\n<pre class=\"prettyprint\">&lt;Annotations Target=\"Default.Container\/Products\"&gt;\r\n    &lt;Annotation Term=\"Org.OData.Capabilities.V1.ReadRestrictions\"&gt;\r\n        &lt;Record&gt;\r\n            &lt;PropertyValue Property=\"Permissions\"&gt;\r\n                &lt;Collection&gt;\r\n                    &lt;Record&gt;\r\n                        &lt;PropertyValue Property=\"SchemeName\" String=\"Scheme\" \/&gt;\r\n                        &lt;PropertyValue Property=\"Scopes\"&gt;\r\n                            &lt;Collection&gt;\r\n                                &lt;Record&gt;\r\n                                    &lt;PropertyValue Property=\"Scope\" String=\"Product.Read\" \/&gt;\r\n                                &lt;\/Record&gt;\r\n                            &lt;\/Collection&gt;\r\n                        &lt;\/PropertyValue&gt;\r\n                    &lt;\/Record&gt;\r\n                &lt;\/Collection&gt;\r\n            &lt;\/PropertyValue&gt;\r\n            &lt;PropertyValue Property=\"ReadByKeyRestrictions\"&gt;\r\n                &lt;Record&gt;\r\n                    &lt;PropertyValue Property=\"Permissions\"&gt;\r\n                        &lt;Collection&gt;\r\n                            &lt;Record&gt;\r\n                                &lt;PropertyValue Property=\"SchemeName\" String=\"Scheme\" \/&gt;\r\n                                &lt;PropertyValue Property=\"Scopes\"&gt;\r\n                                    &lt;Collection&gt;\r\n                                        &lt;Record&gt;\r\n                                            &lt;PropertyValue Property=\"Scope\" String=\"Product.ReadByKey\" \/&gt;\r\n                                        &lt;\/Record&gt;\r\n                                    &lt;\/Collection&gt;\r\n                                &lt;\/PropertyValue&gt;\r\n                            &lt;\/Record&gt;\r\n                        &lt;\/Collection&gt;\r\n                    &lt;\/PropertyValue&gt;\r\n                &lt;\/Record&gt;\r\n            &lt;\/PropertyValue&gt;\r\n        &lt;\/Record&gt;\r\n    &lt;\/Annotation&gt;\r\n...\r\n&lt;\/Annotations&gt;\r\n\r\n<\/pre>\n<h3 id=\"adding-the-controller\" class=\"code-line\" data-line=\"217\">Adding the controller<\/h3>\n<p class=\"code-line\" data-line=\"219\">Let&#8217;s create a\u00a0<code>ProductsController<\/code>\u00a0inside the\u00a0<code>Controllers<\/code>\u00a0folder to implement our CRUD operations:<\/p>\n<pre class=\"prettyprint\">using System.Threading.Tasks;\r\nusing Microsoft.AspNet.OData;\r\nusing Microsoft.AspNetCore.Mvc;\r\nusing ODataAuthorizationDemo.Models;\r\n\r\nnamespace ODataAuthorizationDemo.Controllers\r\n{\r\n    public class ProductsController: ODataController\r\n    {\r\n        private AppDbContext _dbContext;\r\n\r\n        public ProductsController(AppDbContext dbContext)\r\n        {\r\n            _dbContext = dbContext;\r\n        }\r\n\r\n        public IActionResult Get()\r\n        {\r\n            return Ok(_dbContext.Products);\r\n        }\r\n\r\n        public IActionResult Get(int key)\r\n        {\r\n            return Ok(_dbContext.Products.Find(key));\r\n        }\r\n\r\n        public async Task&lt;IActionResult&gt; Post([FromBody] Product product)\r\n        {\r\n            _dbContext.Products.Add(product);\r\n            await _dbContext.SaveChangesAsync();\r\n            return Ok(product);\r\n        }\r\n\r\n        public async Task&lt;IActionResult&gt; Update(int key, [FromBody] Delta&lt;Product&gt; delta)\r\n        {\r\n            var product = await _dbContext.Products.FindAsync(key);\r\n            delta.Patch(product);\r\n            _dbContext.Products.Update(product);\r\n            await _dbContext.SaveChangesAsync();\r\n            return Ok(product);\r\n        }\r\n\r\n        public async Task&lt;IActionResult&gt; Delete(int key)\r\n        {\r\n            var product = await _dbContext.Products.FindAsync(key);\r\n            _dbContext.Products.Remove(product);\r\n            await _dbContext.SaveChangesAsync();\r\n            return Ok(product);\r\n        }\r\n    }\r\n}<\/pre>\n<\/div>\n<p data-line=\"219\">At this point you should be able to perform CRUD operations on the\u00a0<code>odata\/Products<\/code>\u00a0endpoint. The permission restrictions that are defined in the metadata do not automatically apply and all requests should still be authorized. To apply these permissions, we&#8217;ll need to configure the WebApi Authorization middleware. But before we can do that, we&#8217;ll need to set up authentication.<\/p>\n<h2 id=\"setting-up-authentication\" class=\"code-line\" data-line=\"279\">Setting up Authentication<\/h2>\n<p class=\"code-line\" data-line=\"281\">While setting up authentication is required for the authorization system to work, WebApi Authorization does not depend on any specific authentication implementation or scheme. For this demo we are going to use a simple cookie-based authentication flow that will make it easy for us to test different scopes and scenarios.<\/p>\n<h3 id=\"configuring-cookie-authenticaton\" class=\"code-line\" data-line=\"283\">Configuring Cookie authenticaton<\/h3>\n<p class=\"code-line\" data-line=\"285\">Add the following statement in the\u00a0<code>ConfigureServices<\/code>\u00a0method in\u00a0<code>Startup.cs<\/code>\u00a0to add authentication to the app:<\/p>\n<pre class=\"prettyprint\">services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)\r\n    .AddCookie();<\/pre>\n<p data-line=\"285\">You may need the following using statements:<\/p>\n<pre class=\"prettyprint\">using Microsoft.AspNetCore.Authentication.Cookies;<\/pre>\n<p data-line=\"285\">Add the authentication middleware to in the\u00a0<code>Configure<\/code>\u00a0method between\u00a0<code>app.UseRouting()<\/code>\u00a0and\u00a0<code>app.UseEndpoints<\/code>\u00a0(the order matters):<\/p>\n<pre class=\"prettyprint\">app.UseRouting();\r\n\r\napp.UseAuthentication();\r\n            \r\napp.UseEndpoints(endpoints =&gt; \/* ... *\/)<\/pre>\n<h3 id=\"authentication-controller\" class=\"code-line\" data-line=\"308\">Authentication controller<\/h3>\n<p class=\"code-line\" data-line=\"310\">Let&#8217;s create an authentication controller to handle our sign-in and sign-out flows.<\/p>\n<p class=\"code-line\" data-line=\"312\">To make things easy for the demo, we will not require username\/password credentials. The login endpoint will allow the user to specify the scopes that they want via JSON and add those scopes as claims to the user principal. This will allow us to test how different scopes will be handled for different requests by the authorization middleware.<\/p>\n<p class=\"code-line\" data-line=\"315\">Our JSON body will look like:<\/p>\n<pre class=\"prettyprint\">{\r\n    \"RequestedScopes\": [\"Product.Read\", \"Product.Insert\"]\r\n}<\/pre>\n<p data-line=\"315\">Let&#8217;s create a model class to represent such a payload. In the\u00a0<code>Models<\/code>\u00a0folder, create a class\u00a0<code>LoginData<\/code>\u00a0with the following code:<\/p>\n<pre class=\"prettyprint\">namespace ODataAuthorizationDemo.Models\r\n{\r\n    public class LoginData\r\n    {\r\n        public string[] RequestedScopes { get; set; }\r\n    }\r\n}<\/pre>\n<p data-line=\"315\">Then, in the\u00a0<code>Controllers<\/code>\u00a0folder, create the following\u00a0<code>AuthController<\/code>\u00a0class:<\/p>\n<pre class=\"prettyprint\">using System.Linq;\r\nusing System.Security.Claims;\r\nusing System.Threading.Tasks;\r\nusing Microsoft.AspNetCore.Authentication;\r\nusing Microsoft.AspNetCore.Authentication.Cookies;\r\nusing Microsoft.AspNetCore.Http;\r\nusing Microsoft.AspNetCore.Mvc;\r\nusing ODataAuthorizationDemo.Models;\r\n\r\nnamespace ODataAuthorizationDemo.Controllers\r\n{\r\n    [Route(\"[controller]\")]\r\n    [ApiController]\r\n    public class AuthController : ControllerBase\r\n    {\r\n        [HttpPost]\r\n        [Route(\"login\")]\r\n        public async Task&lt;IActionResult&gt; Login([FromBody] LoginData data)\r\n        {\r\n            \/\/ create a claim for each requested scope\r\n            var claims = data.RequestedScopes.Select(s =&gt; new Claim(\"Scope\", s));\r\n\r\n            var claimsIdentity = new ClaimsIdentity(\r\n                claims, CookieAuthenticationDefaults.AuthenticationScheme);\r\n\r\n            var user = new ClaimsPrincipal(claimsIdentity);\r\n\r\n            await HttpContext.SignInAsync(\r\n                CookieAuthenticationDefaults.AuthenticationScheme,\r\n                user);\r\n\r\n            return Ok();\r\n        }\r\n\r\n        [HttpPost]\r\n        [Route(\"logout\")]\r\n        public async Task&lt;IActionResult&gt; Logout()\r\n        {\r\n            await HttpContext.SignOutAsync(\r\n                CookieAuthenticationDefaults.AuthenticationScheme);\r\n\r\n            return Ok();\r\n        }\r\n    }\r\n}<\/pre>\n<div>\n<div>To\u00a0test\u00a0our\u00a0login\u00a0flow,\u00a0we\u00a0make\u00a0the\u00a0following\u00a0POST\u00a0request\u00a0to\u00a0the\u00a0login\u00a0endpoint\u00a0using\u00a0a\u00a0tool\u00a0like\u00a0Postman:<\/div>\n<\/div>\n<div><\/div>\n<div>\n<pre class=\"prettyprint\">POST \/auth\/login\r\n\r\n{\r\n   \"RequestedScopes\": [\"Product.Read\", \"Product.Delete\"]\r\n}<\/pre>\n<\/div>\n<div>\n<p class=\"code-line\" data-line=\"394\">This will create the auth cookie.<\/p>\n<p class=\"code-line\" data-line=\"396\">To log out, we make the following POST request without a body:<\/p>\n<pre class=\"prettyprint\">POST \/auth\/logout\r\n<\/pre>\n<\/div>\n<div>\n<div>Now\u00a0that\u00a0we\u00a0have\u00a0authentication\u00a0set\u00a0up,\u00a0we\u00a0can\u00a0finally\u00a0set\u00a0up\u00a0authorization.<\/div>\n<\/div>\n<div><\/div>\n<div>\n<h2 id=\"adding-authorization\" class=\"code-line code-active-line\" data-line=\"403\">Adding authorization<\/h2>\n<p class=\"code-line\" data-line=\"405\">The authorization middleware will compare the scopes that the authenticated user has to the ones required for the current request based on the model&#8217;s restriction annotations. Since there are many ways in which the scopes could be stored, we need to tell the middleware how to extract the scopes from the current user.<\/p>\n<p class=\"code-line\" data-line=\"408\">Modify\u00a0<code>ConfigureServices<\/code>\u00a0to match the following:<\/p>\n<pre class=\"prettyprint\">public void ConfigureServices(IServiceCollection services)\r\n{\r\n    services.AddDbContext&lt;AppDbContext&gt;(opt =&gt; opt.UseInMemoryDatabase(\"ODataAuthDemo\"));\r\n\r\n    services.AddOData()\r\n        .AddAuthorization(options =&gt;\r\n        {\r\n            options.ScopesFinder = context =&gt;\r\n            {\r\n                var userScopes = context.User.FindAll(\"Scope\").Select(claim =&gt; claim.Value);\r\n                return Task.FromResult(userScopes);\r\n            };\r\n\r\n            options.ConfigureAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)\r\n                .AddCookie();\r\n        });\r\n\r\n    services.AddRouting();\r\n}<\/pre>\n<\/div>\n<p class=\"code-line\" data-line=\"432\">In the above code, we add a call to\u00a0<code>.AddAuthorization()<\/code>\u00a0to the odata builder in order to add the WebApi Authorization services. We configure it such that it will extract the scopes from the user claims that were added by the authentication system. We do this by providing a handler method to the\u00a0<code>options.ScopesFinder<\/code>\u00a0property. We can also configure authentication directly using\u00a0<code>options.ConfigureAuthentication<\/code>.<\/p>\n<p class=\"code-line\" data-line=\"434\">We also need to add\u00a0<code>app.UseODataAuthorization()<\/code>\u00a0after\u00a0<code>app.UseAuthentication()<\/code>\u00a0in the\u00a0<code>Configure()<\/code>\u00a0in order to add the middleware:<\/p>\n<pre class=\"prettyprint\">public void Configure(IApplicationBuilder app, IWebHostEnvironment env)\r\n{\r\n    if (env.IsDevelopment())\r\n    {\r\n        app.UseDeveloperExceptionPage();\r\n    }\r\n\r\n    app.UseRouting();\r\n\r\n    app.UseAuthentication();\r\n\r\n    app.UseODataAuthorization();\r\n    \r\n\r\n    app.UseEndpoints(endpoints =&gt;\r\n    {\r\n        endpoints.Expand().Filter().Count().OrderBy();\r\n        endpoints.MapODataRoute(\"odata\", \"odata\", AppEdmModel.GetModel());\r\n    });\r\n}<\/pre>\n<h2 id=\"testing\" class=\"code-line\" data-line=\"460\">Testing<\/h2>\n<p class=\"code-line\" data-line=\"462\">Let&#8217;s start off by logging in without any scopes:<\/p>\n<pre class=\"prettyprint\">POST \/auth\/login\r\n\r\n{ \"RequestedScopes\": [] }<\/pre>\n<p class=\"code-line\" data-line=\"470\">If we attempt any of the CRUD requests to\u00a0<code>\/odata\/Products<\/code>, we should got a\u00a0<code>403 Forbidden<\/code>\u00a0response because we don&#8217;t have the required scopes to access these endpoints.<\/p>\n<p class=\"code-line\" data-line=\"472\">Now let&#8217;s logout:<\/p>\n<pre class=\"prettyprint\">POST \/auth\/logout\r\n<\/pre>\n<p data-line=\"472\">And request\u00a0<code>Product.Read<\/code>\u00a0and\u00a0<code>Product.Create<\/code>\u00a0scopes:<\/p>\n<pre class=\"prettyprint\">POST \/auth\/login\r\n\r\n{\r\n    \"RequestedScopes\": [\"Product.Read\", \"Product.Create\"]\r\n}<\/pre>\n<p class=\"code-line\" data-line=\"486\">Now we should be able to access\u00a0<code>GET \/odata\/Products<\/code>,\u00a0<code>GET \/odata\/Products({key})<\/code>\u00a0and\u00a0<code>POST \/odata\/Products<\/code>.<\/p>\n<p class=\"code-line\" data-line=\"488\">We should get an error if we try to access\u00a0<code>DELETE \/odata\/Products({key})<\/code>\u00a0or\u00a0<code>PATCH\/PUT \/odata\/Products({key})<\/code><\/p>\n<p class=\"code-line\" data-line=\"490\"><strong>Note<\/strong>: Normally a\u00a0<code>403 Forbidden<\/code>\u00a0error response would be returned, but in this sample it might return a\u00a0<code>404<\/code>\u00a0error instead. This is because the cookie authentication handler attempts to redirect to a login page by default when authorization fails. And since this page does not exist in our sample application, a\u00a0<code>404 Not found<\/code>\u00a0error is returned.<\/p>\n<h2 data-line=\"490\">Feedback<\/h2>\n<p data-line=\"490\">We are looking forward to getting your feedback on this library before a general version is released. The library is open-source, you can report issues and contribute pull-requests on the GitHub repository: <a href=\"https:\/\/github.com\/OData\/WebApiAuthorization\">https:\/\/github.com\/OData\/WebApiAuthorization<\/a><\/p>\n<p data-line=\"490\">You can also find more information about the library from the <a href=\"https:\/\/docs.microsoft.com\/en-us\/odata\/webapiauth\/overview\">official documentation<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post introduces the Web API Authorization library which automatically enforces the permissions defined in your OData model on your Web API endpoints.<\/p>\n","protected":false},"author":20326,"featured_media":3253,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-4330","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-odata"],"acf":[],"blog_post_summary":"<p>This post introduces the Web API Authorization library which automatically enforces the permissions defined in your OData model on your Web API endpoints.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/4330","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/users\/20326"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/comments?post=4330"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/4330\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media\/3253"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media?parent=4330"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/categories?post=4330"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/tags?post=4330"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}