How to Add Openidconnect via Identityserver4 to ASP.NET Core Serverside Blazor Web App

How to add OpenIdConnect via IdentityServer4 to ASP.NET Core ServerSide Blazor web app?

Your code suffers from a couple of maladies... The main issue is that your code provides no authentication challenge request mechanism that enables redirection to an authenticating agent such as IdentityServer. This is only possible with HttpContext, which is not available in SignalR (Blazor Server App). To solve this issue we'll add a couple of Razor pages where the HttpContext is available. More in the answer...

The following is a complete and working solution to the question:

  1. Create a Blazor Server App.
  2. Install-Package Microsoft.AspNetCore.Authentication.OpenIdConnect -Version 3.1.0
  3. Create a component named LoginDisplay (LoginDisplay.razor), and place it in the Shared folder. This component is used in the MainLayout component

    <AuthorizeView>
    <Authorized>
    <a href="logout">Hello, @context.User.Identity.Name !</a>
    <form method="get" action="logout">
    <button type="submit" class="nav-link btn btn-link">Log out</button>
    </form>
    </Authorized>
    <NotAuthorized>
    <a href="login?redirectUri=/">Log in</a>
    </NotAuthorized>
    </AuthorizeView>

    Add the LoginDisplay component to the MainLayout component, just above the About
    anchor element, like this

    <div class="top-row px-4">
    <LoginDisplay />
    <a href="https://learn.microsoft.com/aspnet/" target="_blank">About</a>
    </div>

Note: In order to redirect requests for login and for logout to IdentityServer, we have to create two Razor pages as follows:
1. Create a Login Razor page Login.cshtml (Login.cshtml.cs) and place them in the Pages folder as follow:

Login.cshtml.cs

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.IdentityModel.Tokens;

public class LoginModel : PageModel
{
public async Task OnGet(string redirectUri)
{
await HttpContext.ChallengeAsync("oidc", new
AuthenticationProperties { RedirectUri = redirectUri } );
}
}

This code starts the challenge for the Open Id Connect authentication scheme you defined in the Startup class.


  1. Create a Logout Razor page Logout.cshtml (Logout.cshtml.cs) and place them in the Pages folder as well:

    Logout.cshtml.cs

    using Microsoft.AspNetCore.Authentication;

    public class LogoutModel : PageModel
    {
    public async Task<IActionResult> OnGetAsync()
    {
    await HttpContext.SignOutAsync();
    return Redirect("/");
    }
    }

This code signs you out, redirecting you to the Home page of your Blazor app.

Replace the code in App.razor with the following code:

@inject NavigationManager NavigationManager

<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@{
var returnUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);

NavigationManager.NavigateTo($"login?redirectUri={returnUrl}", forceLoad: true);

}

</NotAuthorized>
<Authorizing>
Wait...
</Authorizing>
</AuthorizeRouteView>
</Found>
<NotFound>

<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>

</NotFound>

</Router>
</CascadingAuthenticationState>

Replace the code in the Startup class with the following:

using Microsoft.AspNetCore.Authentication.OpenIdConnect; 
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using System.Net.Http;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Logging;

public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddAuthorizationCore();
services.AddSingleton<WeatherForecastService>();

services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultAuthenticateScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme =
OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://demo.identityserver.io/";
options.ClientId = "interactive.confidential.short";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.UseTokenLifetime = false;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.TokenValidationParameters = new
TokenValidationParameters
{
NameClaimType = "name"
};

options.Events = new OpenIdConnectEvents
{
OnAccessDenied = context =>
{
context.HandleResponse();
context.Response.Redirect("/");
return Task.CompletedTask;
}
};
});

}

// This method gets called by the runtime. Use this method to configure
the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/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();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
}

IMPORTANT: in all the code sample above you'll have to add using statements as necessary. Most of them are provided by default. The using provided here are those necessary to enable the authentication and authorization flow.

  • Run your app, click on the log in button to authenticate. You are being redirected to IdentityServer test server which allows you to perform an OIDC login. You may enter a user name: bob and password bob, and after click the OK button, you'll be redirected to your home page. Note also that you can use the external login provider Google (try it). Note that after you've logged in with identity server, the LoginDisplay component displays the string "Hello, ".

Note: While you're experimenting with your app, you should clear the browsing data, if you want to be redirected to the identity server's login page, otherwise, your browser may use the cached data. Remember, this is a cookie-based authorization mechanism...

Note that creating a login mechanism as is done here does not make your app more secured than before. Any user can access your web resources without needing to log in at all. In order to secure parts of your web site, you have to implement authorization as well, conventionally, an authenticated user is authorized to access secured resource, unless other measures are implemented, such as roles, policies, etc. The following is a demonstration how you can secure your Fetchdata page from unauthorized users (again, authenticated user is considered authorized to access the Fetchdata page).

  1. At the top of the Fetchdata component page add the @attribute directive for the Authorize attribute, like this: @attribute [Authorize]
    When an unauthenticated user tries to access the Fetchdata page, the AuthorizeRouteView.NotAuthorized delegate property is executed, so we can add some code to redirect the user to the same identity server's login page to authenticate.
  2. The code within the NotAuthorized element looks like this:


    <NotAuthorized>
    @{
    var returnUrl =
    NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
    NavigationManager.NavigateTo($"login?redirectUri=
    {returnUrl}", forceLoad: true);
    }
    </NotAuthorized>

This retrieves the url of the last page you were trying to access, the Fetchdata page, and then navigates to the Login Razor page from which a password challenge is performed, that is, the user is redirected to the identity server's login page to authenticate.

After the user has authenticated he's redirected to the Fetchdata page.

Good luck...

OpenIDConnect in Blazor WebAssembly with an IdentityServer4 + ASP.NET Core Identity postgresql database without redirect

@enet yes, that is exactly what I would like. I know I can edit the login page to look how I want it too, but I dont want to have to leave the domain of the blazor wasm for that to do it

What you wish to do is actually avoid redirection to the IdentityServer4 server, but alas, redirection is the heart of the OpenID Connect flow...

I can only think about a solution common before the Blazor team created the current WebAssembly authentication system: Use JWT Token Authentication...

Do the following:

  • Create a WebAssembly hosted app.
  • Create a Login page, register page, etc.

With WebAssembly hosted app, the front-end (browser) and the backend (server) belong to the same domain. You can add an account controller to the Controllers folder with a Login end point, which will be accessed from the front-end, using HTTP calls, with the users' credentials gathered by your Login page. The code in the Login end point should authenticate the user, perhaps using the Identity system (default), and then creates a JWT Token which is passed to the front-end
(Could be stored in the local storage). Now, whenever you want to access a protected web api end point, you add the JWT Token to the HttpClient object's header.

Note: I do not recommend adopting this on account of the time and knowledge it require. Of course, it's feasible, and that's how we used to code before.

I did not have much time to answer your question in detail and in order, so if you have questions, don't hesitate to ask.

Thank you for your response! Maybe I just dont get it then... If I want login to any other site I am never redirected to some other domain to login. Does that mean all those sites dont use OIDC?

If what you're stating is correct, then the answer is yes. The flow of OpenID Connect requires redirection. You can't and you should not authenticate the user on the client (browser). You can redirect him to an identity provider like IdnetityServer4 if you're using OpenID Connect, as for instance, or perform an HTTP call to send the user's credentials to a Web Api end point to authenticate, returning a JWT Token. There are other methods of authenticating the user, none of which takes place on the front-end. As you want your web application as well as the server that serves the application and contains Web Api end points, you should consider my suggestion above. This is a viable solution, mind you, and I did not really meant to deter you from adopting it; the point is that Blazor offers you amazing functionality, well tested and secured which mostly requires you to configure some settings, etc., while implementing it on your own requires some knowledge and time. But of course you can find examples of JWT Token authentication for Blazor.

OIDC authentication in server-side Blazor

The following is a complete and working solution to the question:

First off, you'll need to provide an authentication challenge request mechanism that enables redirection to an authenticating agent such as IdentityServer. This is only possible with HttpContext, which is not available in SignalR (Blazor Server App). To solve this issue we'll add a couple of Razor pages where the HttpContext is available. More in the answer...

Create a Blazor Server App.

Install-Package Microsoft.AspNetCore.Authentication.OpenIdConnect -Version 3.1.0 or later.

Create a component named LoginDisplay (LoginDisplay.razor), and place it in the
Shared folder. This component is used in the MainLayout component:

<AuthorizeView>
<Authorized>
<a href="logout">Hello, @context.User.Identity.Name !</a>
<form method="get" action="logout">
<button type="submit" class="nav-link btn btn-link">Log
out</button>
</form>
</Authorized>
<NotAuthorized>
<a href="login?redirectUri=/">Log in</a>
</NotAuthorized>
</AuthorizeView>

Add the LoginDisplay component to the MainLayout component, just above the About
anchor element, like this

<div class="top-row px-4">
<LoginDisplay />
<a href="https://learn.microsoft.com/aspnet/" target="_blank">About</a>
</div>

Note: In order to redirect requests for login and for logout to IdentityServer, we have to create two Razor pages as follows:

  1. Create a Login Razor page Login.cshtml (Login.cshtml.cs) and place them in the Pages folder as follow:
    Login.cshtml.cs


using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.IdentityModel.Tokens;

public class LoginModel : PageModel
{
public async Task OnGet(string redirectUri)
{
await HttpContext.ChallengeAsync("oidc", new
AuthenticationProperties { RedirectUri = redirectUri } );
}
}

This code starts the challenge for the Open Id Connect authentication scheme you defined in the Startup class.


  1. Create a Logout Razor page Logout.cshtml (Logout.cshtml.cs) and place them in the Pages folder as well:
    Logout.cshtml.cs


using Microsoft.AspNetCore.Authentication;

public class LogoutModel : PageModel
{
public async Task<IActionResult> OnGetAsync()
{
await HttpContext.SignOutAsync();
return Redirect("/");
}
}

This code signs you out, redirecting you to the Home page of your Blazor app.

Replace the code in App.razor with the following code:

@inject NavigationManager NavigationManager

<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@{
var returnUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);

NavigationManager.NavigateTo($"login?redirectUri={returnUrl}", forceLoad: true);

}
</NotAuthorized>
<Authorizing>
Wait...
</Authorizing>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>

Replace the code in the Startup class with the following:

using Microsoft.AspNetCore.Authentication.OpenIdConnect; 
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddAuthorizationCore();
services.AddSingleton<WeatherForecastService>();

services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultAuthenticateScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme =
OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://demo.identityserver.io/";
options.ClientId = "interactive.confidential.short";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.UseTokenLifetime = false;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.TokenValidationParameters = new
TokenValidationParameters
{
NameClaimType = "name"
};

options.Events = new OpenIdConnectEvents
{
OnAccessDenied = context =>
{
context.HandleResponse();
context.Response.Redirect("/");
return Task.CompletedTask;
}
};
});
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/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();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
}

IMPORTANT: in all the code sample above you'll have to add using statements as necessary. Most of them are provided by default. The using provided here are those necessary to enable the authentication and authorization flow.

  • Run your app, click on the log in button to authenticate. You are being redirected to IdentityServer test server which allows you to perform an OIDC login. You may enter a user name: bob and password bob, and after click the OK button, you'll be redirected to your home page. Note also that you can use the external login provider Google (try it). Note that after you've logged in with identity server, the LoginDisplay component displays the string "Hello, <your user name>".

Note: While you're experimenting with your app, you should clear the browsing data, if you want to be redirected to the identity server's login page, otherwise, your browser may use the cached data. Remember, this is a cookie-based authorization mechanism...

Note that creating a login mechanism as is done here does not make your app more secured than before. Any user can access your web resources without needing to log in at all. In order to secure parts of your web site, you have to implement authorization as well, conventionally, an authenticated user is authorized to access secured resource, unless other measures are implemented, such as roles, policies, etc. The following is a demonstration how you can secure your Fetchdata page from unauthorized users (again, authenticated user is considered authorized to access the Fetchdata page).

At the top of the Fetchdata component page add the @attribute directive for the Authorize attribute, like this: @attribute [Authorize]
When an unauthenticated user tries to access the Fetchdata page, the AuthorizeRouteView.NotAuthorized delegate property is executed, so we can add some code to redirect the user to the same identity server's login page to authenticate.

The code within the NotAuthorized element looks like this:

<NotAuthorized>
@{
var returnUrl =
NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
NavigationManager.NavigateTo($"login?redirectUri=
{returnUrl}", forceLoad: true);
}
</NotAuthorized>

This retrieves the url of the last page you were trying to access, the FetchData page, and then navigates to the Login Razor page from which a password challenge is performed, that is, the user is redirected to the identity server's login page to authenticate.

After the user has authenticated they are redirected to the FetchData page.

New Blazor project with connection to local Identity Server 4

No,that option is used to connect Azure AD B2C application,one of Microsoft's cloud identity service .

If you want to connect to local Identity Server 4 , you can firstly installl package IdentityServer4 , and then add authentication in blazor app , use OIDC middleware to connect to IDS4 :

services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;

options.ClientId = "mvcBlazor";
options.SaveTokens = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
});

services.AddMvcCore(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});

and

app.UseAuthentication();

You can refer to below article for code sample and explanation :

https://nightbaker.github.io/blazor/identityserver4/serverapp/2019/08/29/blazor-serverapp-identity-server-4/

Blazor(server-side) with IdentityServer4

Hope this can help you.

I need the token, because I want to call a API protected by this token. You must Add thing to the Startup.cs and to retrieve it later you can look at the class below Startup.cs

Here is what needed in Startup.cs (the importen part is HttpContextAccessor).

You need the following using i Startup.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;

And then (still in Startup.cs):

public void ConfigureServices(IServiceCollection services)
{

services.AddAuthentication(options =>
{
options.DefaultScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://localhost:5001/";
options.ClientId = "AuthCodeClient";
options.ClientSecret = "verrystrongpwd";
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("user.management.api");
options.Scope.Add("identity-provider.Api");
options.CallbackPath = "/signin-oidc";
});

services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});

services.AddRazorPages();
services.AddServerSideBlazor();

// HttpContextAccessor
services.AddHttpContextAccessor();
services.AddScoped<HttpContextAccessor>();

........ maybe more code....

Her is the class where I retrieve info from HttpContext, as access_token

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace Management.Ui.Services
{
public class TokenContainer
{
private readonly IHttpContextAccessor _httpContextAccessor;

public TokenContainer(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}

protected async Task AddRequestHeaders(HttpClient httpClient)
{
var accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync("access_token");
httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
}
}

How to authorize a Blazor WebAssembly SPA app using Identity Server

March 12th, 2020

To add OIDC to an existing Blazor WASM app using an existing OAuth identity provider read Secure an ASP.NET Core Blazor WebAssembly standalone app with the Authentication library.

The new Microsoft.AspNetCore.Components.WebAssembly.Authentication support automatic silent renew.

If you prefere to use a configuration file instead of hard coded values, you can setup the app like this

Visit theidserver.herokuapp.com/ for a full fonctional sample.

  • Upgrade your app to 3.2.0 Preview 2
    Read Upgrade an existing project paragraph

  • Add package Microsoft.AspNetCore.Components.WebAssembly.Authentication

dotnet add package Microsoft.AspNetCore.Components.WebAssembly.Authentication::3.2.0-preview2.20160.5
  • Add AuthenticationService.js to index.html
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<app>Loading...</app>
...
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
</body>

</html>
{
"authority": "https://myidp.com", // Uri to your IDP server
"client_id": "myclientid", // Your client id
"redirect_uri": "https://localhost:44333/authentication/login-callback", // Uri to the application login callback
"post_logout_redirect_uri": "https://localhost:44333/authentication/logout-callback", // Uri to the application logout callback
"response_type": "code", // OAuth flow response type. For `authorization_code` flow the response type is 'code'. For `implicit` flow, the response type is 'id_token token'
"scope": "BlazorIdentityServer.ServerAPI openid profile" // list of scope your application wants
}
  • Configure Api authorization to read config from your oidc.json file
    Update your Program.cs to be :
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;

namespace BlazorIdentityServer.Client
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");

builder.Services.AddBaseAddressHttpClient();
builder.Services.AddApiAuthorization(options => options.ProviderOptions.ConfigurationEndpoint = "oidc.json");

await builder.Build().RunAsync();
}
}
}


Related Topics



Leave a reply



Submit