The article shows how to implement an OData client from an ASP.NET Core application. Bearer token authorization is used to secure the API.

Code: https://github.com/damienbod/AspNetCoreOData

This blog is part 2 from this blog:

Part 1: OData with ASP.NET Core

History

2020-07-06 Updated .NET Core 3.1, IdentityServer4 4.0.2

Setting up the applications

Three applications are used to implement this, the StsServerIdentity, which is the secure token service implemented using IdentityServer4, the AspNetCoreOData.Service which hosts the OData API, and the AspNetCoreOData.Client which is the OData client.

Securing the OData API in ASP.NET Core

The ASP.NET Core MVC application AspNetCoreOData.Service secures the API using introspection. The AddIdentityServerAuthentication extension method is used from the IdentityServer4.AccessTokenValidation NuGet package. The Authority must match that from the secure token server, and the other configurations must match the STS configurations for the API, which are defined in the Config.cs file in most IdentityServer4 implementations.

The ODataServiceApiPolicy require claim policy is added for the scope claim with the value ScopeAspNetCoreODataServiceApi.

public void ConfigureServices(IServiceCollection services) { services.AddDbContext<AdventureWorks2016Context>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) .AddIdentityServerAuthentication(options => { options.Authority = "https://localhost:44318"; options.ApiName = "AspNetCoreODataServiceApi"; options.ApiSecret = "AspNetCoreODataServiceApiSecret"; options.RequireHttpsMetadata = true; }); services.AddAuthorization(options => options.AddPolicy("ODataServiceApiPolicy", policy => { policy.RequireClaim("scope", "ScopeAspNetCoreODataServiceApi"); }) ); services.AddOData(); services.AddODataQueryFilter(); services.AddControllers(mvcOptions => mvcOptions.EnableEndpointRouting = false); }

The UseAuthentication is added to the Configure method. Now the API can authorize.

public void Configure(IApplicationBuilder app) { app.UseExceptionHandler("/Home/Error"); app.UseCors("AllowAllOrigins"); app.UseSerilogRequestLogging(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseMvc(b => b.MapODataServiceRoute("odata", "odata", GetEdmModel(app.ApplicationServices) )); }

The OData controller uses the Authorize attribute, and a policy to validate the requests to the API.

using System.Linq; using Microsoft.AspNetCore.Mvc; using AspNetCoreOData.Service.Database; using Microsoft.AspNet.OData.Routing; using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; using Microsoft.AspNetCore.Authorization; namespace AspNetCoreOData.Service.Controllers { [Authorize(Policy = "ODataServiceApiPolicy", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [ODataRoutePrefix("Person")] public class PersonController : ODataController { private AdventureWorks2016Context _db; public PersonController(AdventureWorks2016Context AdventureWorks2016Context) { _db = AdventureWorks2016Context; } [ODataRoute] [EnableQuery(PageSize = 20, AllowedQueryOptions= AllowedQueryOptions.All )] public IActionResult Get() { return Ok(_db.Person.AsQueryable()); }

Implementing an OData Client in ASP.NET Core

The OData client is implemented in an ASP.NET Core MVC application. This application authenticates and authorizes using the OpenID Connect Hybrid Flow, which saves the tokens in a cookie.

public void ConfigureServices(IServiceCollection services) { services.Configure<CookiePolicyOptions>(options => { options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie() .AddOpenIdConnect(options => { options.SignInScheme = "Cookies"; options.Authority = "https://localhost:44318"; options.RequireHttpsMetadata = true; options.ClientId = "AspNetCoreODataClient"; options.ClientSecret = "AspNetCoreODataClientSecret"; options.ResponseType = "code id_token"; options.Scope.Add("ScopeAspNetCoreODataServiceApi"); options.Scope.Add("profile"); options.SaveTokens = true; }); services.AddAuthorization(); services.AddControllersWithViews(); }

The Configure method:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseSerilogRequestLogging(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); }

The home controller then requires that the use is logged in. The access token is got by using the HttpContext.GetTokenAsync which gets the access token from the cookie.

The ODataClientSettings is then used to add the token to the OData client from the Nuget package Simple.OData.V4.Client. The token is added using the BeforeRequest method, and adds the access token as the Authorization HTTP Header with Bearer, space, and then the value.

The code from the package can be found here:

https://github.com/object/Simple.OData.Client

This client can then use the type safe methods from OData client. The example in the following code uses the Expand method.

[Authorize] public class HomeController : Controller { public async Task<IActionResult> Index() { var accessToken = HttpContext.GetTokenAsync("access_token").Result; var client = new ODataClient(SetODataToken("https://localhost:44345/odata", accessToken)); //var client = new ODataClient("https://localhost:44345/odata"); var persons = await client.For<Person>() .Expand(rr => rr.EmailAddress) .Top(7).Skip(7) .FindEntriesAsync(); return View(persons); } private ODataClientSettings SetODataToken(string url, string accessToken) { var oDataClientSettings = new ODataClientSettings(new Uri(url)); oDataClientSettings.BeforeRequest += delegate (HttpRequestMessage message) { message.Headers.Add("Authorization", "Bearer " + accessToken); }; return oDataClientSettings; }

Secure token Service configuration for the API and the OIDC Hybrid Flow

The secure token service is implemented using IdentityServer4. The clients are configured in the Config class. An OIDC Hybrid Flow is configured and a resource API for the AspNetCoreOData.Service.

using IdentityServer4; using IdentityServer4.Models; using Microsoft.Extensions.Configuration; using System.Collections.Generic; namespace StsServerIdentity { public class Config { public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email() }; } public static IEnumerable<ApiScope> GetApiScopes() { return new List<ApiScope> { new ApiScope("ScopeAspNetCoreODataServiceApi", "OData API AspNetCoreOData.Service") }; } public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource("AspNetCoreODataServiceApi") { DisplayName = "OData API AspNetCoreOData.Service", ApiSecrets = { new Secret("AspNetCoreODataServiceApiSecret".Sha256()) }, Scopes = { "ScopeAspNetCoreODataServiceApi"}, UserClaims = { "role", "admin", "user" } } }; } public static IEnumerable<Client> GetClients(IConfigurationSection authConfigurations) { return new List<Client> { new Client { ClientName = "AspNetCoreOData.Client", ClientId = "AspNetCoreODataClient", ClientSecrets = {new Secret("AspNetCoreODataClientSecret".Sha256()) }, AllowedGrantTypes = GrantTypes.Hybrid, AllowOfflineAccess = true, RequireConsent = true, RequirePkce = false, AccessTokenLifetime = 86400, RedirectUris = { "https://localhost:44388/signin-oidc" }, PostLogoutRedirectUris = { "https://localhost:44388/signout-callback-oidc" }, AllowedCorsOrigins = new List<string> { "https://localhost:44388" }, AllowedScopes = new List<string> { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.OfflineAccess, "ScopeAspNetCoreODataServiceApi", "role" } } }; } } }

When the application is run, the OData Client and User authorizes and authenticates, then requests the data from the API, and displays the data in the home index view of the MVC application.

Links

https://github.com/Microsoft/sql-server-samples/releases/tag/adventureworks

https://docs.microsoft.com/en-us/ef/core/get-started/aspnetcore/existing-db

https://blogs.msdn.microsoft.com/odatateam/2018/07/03/asp-net-core-odata-now-available/

http://odata.github.io/

https://blogs.msdn.microsoft.com/odatateam/

https://github.com/object/Simple.OData.Client

https://dotnetthoughts.net/getting-started-with-odata-in-aspnet-core/

https://github.com/damienbod/WebAPIODataV4

https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth

http://docs.identityserver.io/en/release/index.html

https://jwt.io/