Identity Server 4 encore...

Identity Server 4 encore...

In my previous post I created some projects using the .Net project templates as a basis for some further posts. In this post I will set up the Identity Server, and get the authentication and authorisation of the MVC Client and Web Api projects to work via this server.

The Identity Server

The server template includes a Config class that contains Api Resources and Clients, the defaults here need to be updated to include the MVC Client and Web Api projects created in the earlier post:

In the Config.cs file, the ApiScopes collection includes two default values, these can be removed:

        public static IEnumerable<ApiScope> ApiScopes =>
            new ApiScope[]
            {
                new ApiScope("scope1"),
                new ApiScope("scope2"),
            };

And then add the Web Api to the collection:

        public static IEnumerable<ApiScope> ApiScopes =>
            new ApiScope[]
            {
                new ApiScope("WebApi")
            };

The Clients collection includes two default values, again these can be removed:

        public static IEnumerable<Client> Clients =>
            new Client[]
            {
                // m2m client credentials flow client
                new Client
                {
                    ClientId = "m2m.client",
                    ClientName = "Client Credentials Client",

                    AllowedGrantTypes = GrantTypes.ClientCredentials,
                    ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },

                    AllowedScopes = { "scope1" }
                },

                // interactive client using code flow + pkce
                new Client
                {
                    ClientId = "interactive",
                    ClientSecrets = { new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256()) },

                    AllowedGrantTypes = GrantTypes.Code,

                    RedirectUris = { "https://localhost:44300/signin-oidc" },
                    FrontChannelLogoutUri = "https://localhost:44300/signout-oidc",
                    PostLogoutRedirectUris = { "https://localhost:44300/signout-callback-oidc" },

                    AllowOfflineAccess = true,
                    AllowedScopes = { "openid", "profile", "scope2" }
                },
            };

And the MVC Client details can be added, the uris will need to be those of the MVC Client project, so in this case the port has changed to 44319 and the AllowedScopes need to include the name of the ApiScope for the Web Api project created earlier:

        public static IEnumerable<Client> Clients =>
            new Client[]
            {
            new Client[]
            {
                new Client
                {
                    ClientId = "MvcClient",
                    ClientSecrets = { new Secret("secret".Sha256()) },

                    AllowedGrantTypes = GrantTypes.Code,

                    RedirectUris = { "https://localhost:44319/signin-oidc" },
                    PostLogoutRedirectUris = { "https://localhost:44319/signout-callback-oidc" },

                    AllowedScopes = { "openid", "profile", "WebApi" }
                },
            };

The MVC Client

To have the MVC Client use the Identity Server for authentication, references to the Microsoft.AspNetCore.Authentication.OpenIdConnect and IdentityModel NuGet packages are added: openid-nuget.PNG To configure the MVC Client to use the server the ConfigureServices(...) and Configure(...) methods of the Startup class need to be modified:

Authentication is added to the ConfigureServices with the details of the MVC Client. The Authority is the uri for the Identity Server, the ClientId, ClientSecret and Scopes are the same as those configured for the Client on the server:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();

            services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie(options =>
            {
                options.Cookie.Name = "cookie";
            })
            .AddOpenIdConnect("oidc", options =>
            {
                options.Authority = "https://localhost:5001";

                options.ClientId = "MvcClient";
                options.ClientSecret = "secret";
                options.ResponseType = "code";

                options.Scope.Add("WebApi");

                options.SaveTokens = true;
            });
        }

Authentication is then used in the Configure method, here Authorization can be required on all endpoints or it can be more targeted by use the Authorize attribute on the controller classes, I am not going to cover this here:

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                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.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication(); // actually use the auth here
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute()
                    .RequireAuthorization(); // require auth on all endpoints
            });
        }

If you now run the Identity Server and then the MVC Client the Login page from the server will be displayed first and then, once you login, the MVC Client main page is displayed: login-page.PNG The interaction between the server and the client creates a number of cookies which can be seen by using the Developer (F12) tools in the browser: cookies.PNG

The Web Api

The Web Api will accept JwtBearer tokens which will be validated by the Identity Server, so a reference to the Microsoft.AspNetCore.Authentication.JwtBearer NuGet package is added: image.png As with the MVC Client the ConfigureServices(...) and Configure(...) methods of the Startup class need to be modified to configure authentication using the Identity Server.

Authentication is added to the ConfigureServices with the details of the Web Api. The Authority is the uri for the Identity Server. A policy is added to ensure that any token provided include the Web Api Scope set up on the server:

        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApi", Version = "v1" });
            });

            // accepts any access token issued by identity server
            services.AddAuthentication("Bearer")
                .AddJwtBearer("Bearer", options =>
                {
                    options.Authority = "https://localhost:5001";

                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateAudience = false
                    };
                });
            // adds an authorization policy to make sure the token is for scope 'WebApi'
            services.AddAuthorization(options =>
            {
                options.AddPolicy("ApiScope", policy =>
                {
                    policy.RequireAuthenticatedUser();
                    policy.RequireClaim("scope", "WebApi");
                });
            });
        }

Again Authentication is used in the Configure method and Authorization can be required on all endpoints as in the MVC Client:

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApi v1"));
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers().RequireAuthorization();
            });
        }

Now if you run both the Identity Server and the Web Api, when you call the WeatherForecast endpoint via Swagger, you should receive a Server response Code 401 as a Bearer token was not supplied with the call: Swagger401.PNG

MVC Client Calls the WebApi

The Home page of the MVC Client is going to be used to display the Weather Forecast provided by the Web Api. The data is a json array so a reference to NewtonSoft.Json needs to be added to the project: netonsoft-nuget.PNG To call the Web Api a HttpClient will be used, this can be created by a factory component that implements IHttpClientFactory. A default component can be registered in the IServiceCollection by calling AddHttpClient() in the Startup class:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services.AddHttpClient();

            services.AddAuthentication(options =>...)
            .AddCookie(options =>...)
            .AddOpenIdConnect("oidc", options =>...);
        }

The HomeController then needs to be modified to accept the IHttpClientFactory in the constructor, where it will be save in a private field for later:

        private readonly ILogger<HomeController> _logger;
        private readonly IHttpClientFactory _httpClientFactory;

        public HomeController(ILogger<HomeController> logger, IHttpClientFactory httpClientFactory)
        {
            _logger = logger;
            _httpClientFactory = httpClientFactory;
        }

In the Index method a call is made to get an access token from the Identity Server using the GetTokenAsync extension method. The HttpClient is created by the client factory and the access token is then set as the bearer token for the HttpClient. The call is made to the Web Api weather forecast endpoint and the data from the response is added to the ViewBag:

        public async Task<IActionResult> Index()
        {
            try
            {
                var token = await HttpContext.GetTokenAsync("access_token");

                var client = _httpClientFactory.CreateClient();
                client.SetBearerToken(token);

                var response = await client.GetStringAsync(@"https://localhost:44373/WeatherForecast");
                ViewBag.Json = JArray.Parse(response).ToString();
            }
            catch { }


            return View();
        }

To display the data in the view a small piece of html is added to the end of the Index.cshtml file:

<div class="text-center">
    <h2>Output from the Web Api call:</h2>
    @if (string.IsNullOrEmpty(ViewBag.Json))
    {
        <p>No data..</p>
    }
    else
    {
        <p>@ViewBag.Json</p>
    }
</div>

Running the Identity Server, the Web Api and finally the MVC Client. The data from the response of the call to the Web Api is displayed on the client's home page: MVC-With-APIData.PNG The next post describes how to configure Swagger and Postman to use Identity Server as an access token provider. I hope you find this post useful, the source code can be found here in github