Skip to content

Commit

Permalink
Merge pull request #4975 from sanjulamadurapperuma/master
Browse files Browse the repository at this point in the history
Amendments for .NET guide to improve developer experience
  • Loading branch information
sagara-gunathunga authored Jan 21, 2025
2 parents 5088a4a + 6ee2cf8 commit 6b4f6ba
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ Let’s create a file named `Scim2Me.razor` under the `/Components/Pages` direct
}

@code {
private string token;
private string apiResponse;
private string? token;
private string? apiResponse;

protected override async Task OnInitializedAsync()
{
Expand All @@ -104,20 +104,16 @@ Let’s create a file named `Scim2Me.razor` under the `/Components/Pages` direct
else
{
apiResponse = "Access token was not found. Protected API invocation failed.";
Console.WriteLine("No token found in HttpContext.");
}
}
else
{
apiResponse = "Protected API invocation failed due to invalid authentication state.";
Console.WriteLine("HttpContext is null.");
}
}

private async Task CallApi()
{
Console.WriteLine("CallApi invoked.");
@* string accessToken = await HttpContextAccessor.HttpContext.GetTokenAsync("access_token"); *@
if (string.IsNullOrEmpty(token))
{
// Token is not available, handle the case where the user is not authenticated
Expand All @@ -137,7 +133,6 @@ Let’s create a file named `Scim2Me.razor` under the `/Components/Pages` direct
var data = await response.Content.ReadAsStringAsync();
// Format the JSON response into a pretty string for display
apiResponse = FormatJson(data);
Console.WriteLine("token: " + data);
// Do something with the data
}
else
Expand All @@ -155,7 +150,7 @@ Let’s create a file named `Scim2Me.razor` under the `/Components/Pages` direct
{
// Parse the JSON string into an object
var jsonObject = JsonSerializer.Deserialize<JsonElement>(json);

// Convert the object back into a nicely formatted JSON string
return JsonSerializer.Serialize(jsonObject, new JsonSerializerOptions { WriteIndented = true });
}
Expand Down
65 changes: 45 additions & 20 deletions en/asgardeo/docs/complete-guides/dotnet/add-login-and-logout.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ Let's create a file named `PersistingAuthenticationStateProvider.cs` in the root
- The authentication state is persisted across interactions.
- The authentication state is rehydrated correctly.

This approach is particularly useful in scenarios where the application depends on server-side data persistence for authentication state.
This approach is particularly useful in scenarios where the application depends on server-side data persistence for authentication state. You can utilize the following code which performs the above tasks. Make sure to replace `<namespace>` with the appropriate namespace for your application.

```csharp title="PersistingAuthenticationStateProvider.cs"
@using Microsoft.AspNetCore.Components;
@using Microsoft.AspNetCore.Components.Authorization;
@using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Web;

namespace AsgardeoDotNetSample;
namespace <namespace>;

internal sealed class PersistingAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider, IDisposable
{
Expand All @@ -52,11 +52,6 @@ internal sealed class PersistingAuthenticationStateProvider : AuthenticationStat
{
var authenticationState = await GetAuthenticationStateAsync();
var principal = authenticationState.User;

if (principal.Identity?.IsAuthenticated == true)
{
persistentComponentState.PersistAsJson(nameof(UserInfo), UserInfo.FromClaimsPrincipal(principal));
}
}

public void Dispose()
Expand All @@ -76,10 +71,9 @@ The Login and Logout route builder adds the `/login` and `/logout` routes to the
Let's create a file named `LoginLogoutEndpointRouteBuilderExtensions.cs` in the root directory and add the following code.

```csharp title="LoginLogoutEndpointRouteBuilderExtensions.cs"
@using Microsoft.AspNetCore.Authentication;
@using Microsoft.AspNetCore.Authentication.Cookies;
@using Microsoft.AspNetCore.Authentication.OpenIdConnect;
@using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;

namespace Microsoft.AspNetCore.Routing;

Expand Down Expand Up @@ -126,7 +120,7 @@ The `GetAuthProperties` method is used to build the authentication properties, s

## UserInfo Class

The `UserInfo` class represents information about an authenticated user and provides methods to map between a `ClaimsPrincipal` and this strongly typed representation. It serves as a way to expose more structured and manageable user information in an ASP.NET Core application.
The `UserInfo` class represents information about an authenticated user and provides methods to map between a `ClaimsPrincipal` and this strongly typed representation. It serves as a way to expose more structured and manageable user information in an ASP.NET Core application. While this will not be used for the login and logout functionality in this guide, it will be useful as an extension point for other parts of your application where user information needs to be retrieved.

The class encapsulates key user attributes such as:

Expand All @@ -139,9 +133,9 @@ These attributes are derived from claims associated with the authenticated user'
Create a file named `UserInfo.cs` in the root directory and add the following code.

```csharp title="UserInfo.cs"
@using System.Security.Claims;
using System.Security.Claims;

namespace AsgardeoDotNetSample;
namespace <namepsace>;

public sealed class UserInfo
{
Expand Down Expand Up @@ -175,6 +169,21 @@ public sealed class UserInfo
}
```

Once this is done, navigate to the `PersistingAuthenticationStateProvider.cs` file and add the following code line in order to persist the user details into the `PersistentComponentState`.

```csharp hl_lines="6-9"
private async Task OnPersistingAsync()
{
var authenticationState = await GetAuthenticationStateAsync();
var principal = authenticationState.User;

if (principal.Identity?.IsAuthenticated == true)
{
persistentComponentState.PersistAsJson(nameof(UserInfo), UserInfo.FromClaimsPrincipal(principal));
}
}
```

## Home Page Setup

The classes implemented up to now lay the foundation for the home page where we will be adding the login and logout buttons.
Expand All @@ -184,6 +193,7 @@ Navigate to the `Home.razor` file under the `/Components/Pages` directory and ad
```csharp title="Home.razor"
@implements IDisposable
@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.Authorization

@code {
private string? currentUrl;
Expand All @@ -208,7 +218,15 @@ The above code implements the `IDisposable` interface to manage lifecycle events

The current URL (`currentUrl`) is tracked by subscribing to the `LocationChanged` event from `NavigationManager`. When the URL changes, it updates `currentUrl` and triggers a re-render using `StateHasChanged()`, while the `Dispose` method unsubscribes from `LocationChanged`.

Next, let's add the login and logout buttons to the `Home.razor` page as follows above the code that was previously added.
You can remove the following default code from the `Home.razor` file:

```html title="Home.razor"
<h1>Hello, world!</h1>

Welcome to your new app.
```

Next, let's add the login and logout buttons to the `Home.razor` page as follows above the code that was previously added under "@code".

```html title="Home.razor"
<div class="nav-item px-3">
Expand Down Expand Up @@ -268,9 +286,11 @@ else
builder.Services.AddSingleton(httpClient);
```

Add the below `FetchJwks` method which will perform the invocation of the endpoint passed as a parameter in order to parse the `JsonWebKeySet`.
Add the below `FetchJwks` method which will perform the invocation of the endpoint passed as a parameter in order to parse the `JsonWebKeySet` class in the `Microsoft.IdentityModel.Tokens` namespace .

```csharp title="Program.cs"
using Microsoft.IdentityModel.Tokens;

JsonWebKeySet FetchJwks(string url)
{
var result = httpClient.GetAsync(url).Result;
Expand All @@ -288,6 +308,9 @@ JsonWebKeySet FetchJwks(string url)
We can now register the services required for the authentication services in the `Program.cs` file as given below.

```csharp title="Program.cs"
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.IdentityModel.JsonWebTokens;

const string ASGARDEO_OIDC_SCHEME = "AsgardeoOidc";

builder.Services.AddAuthentication(ASGARDEO_OIDC_SCHEME)
Expand Down Expand Up @@ -330,9 +353,11 @@ The environment variables that were added in a prior step are utilized to create

For the purpose of this guide, the `SaveTokens` property is set to `true`, so the application will store the access token, refresh token, and ID token in the authentication properties and simplify the token persistence.

The following `WebApplicationBuilder` services need to be configured in order to enable authorization and also add the custom `AuthenticationStateProvider` class previously created as a scoped service:
The following `WebApplicationBuilder` services need to be configured in order to enable authorization and also add the custom `AuthenticationStateProvider` class previously created as a scoped service. Note that the application namespace usage need to be added as well.

```csharp title="Program.cs"
using asgardeo_dotnet;

builder.Services.AddAuthorization();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<AuthenticationStateProvider, PersistingAuthenticationStateProvider>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ read_time: 2 min

The information that was noted in step 3 can now be utilized in the created .NET application.

For the purpose of this guide, these properties will be added to the `launchSettings.json` file. You can, however, use any other configuration source according to your preference. The following are the properties that you need to configure.
For the purpose of this guide, these properties will be added to the `/Properties/launchSettings.json` file. You can, however, use any other configuration source according to your preference. The following are the properties that you need to configure.

- Authorization Endpoint
- Token Endpoint
Expand All @@ -17,7 +17,7 @@ For the purpose of this guide, these properties will be added to the `launchSett
- Client Secret
- Metadata address

An example configuration is shown below (placeholders have to be replaced with the actual values).
An example configuration is shown below (placeholders have to be replaced with the actual values). Additionally, you can remove the `http` profile from the `launchSettings.json` file as we will only be utilizing the https profile for the purposes of this guide.

```json hl_lines="10-17"
"profiles": {
Expand All @@ -35,9 +35,11 @@ An example configuration is shown below (placeholders have to be replaced with t
"LOGOUT_URI": "https://api.asgardeo.io/t/<org>/oidc/logout",
"AUTHORITY": "https://api.asgardeo.io/t/<org>/",
"CLIENT_ID": "<client_id>",
"CLIENT_SECRET": "<client_id>",
"CLIENT_SECRET": "<client_secret>",
"METADATA_ADDRESS": "https://api.asgardeo.io/t/<org>/oauth2/token/.well-known/openid-configuration"
}
},
}
```
```

Now that we have configured the authentication properties, we can proceed to implement the authentication logic in the .NET application.
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ dotnet run

![Run .NET app]({{base_path}}/complete-guides/dotnet/assets/img/image5.png){: width="800" style="display: block; margin: 0;"}

You should now see the default Blazor Web App template running in your browser as shown above. Next we will look into configuring the authentication properties.
You should now see the default Blazor Web App template running in your browser as shown above. Next we will look into configuring the authentication properties.
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ Next, add the following in the HTML section of the file. This will retrieve the
}
```

It is also possible to retrieve properties such as the name of the user from the context. In this case, we will display the user's name in the logout button by adding `@context.User.Identity?.Name`.
It is also possible to retrieve properties such as the name of the user from the context. In this case, we will display the user's name in the logout button by adding `@context.User.Identity?.Name` to the logout button as highlighted below.

```html title="Home.razor"
```html title="Home.razor" hl_lines="6"
<form action="authentication/logout" method="post">
<AntiforgeryToken />
<input type="hidden" name="ReturnUrl" value="@currentUrl" />
Expand All @@ -74,6 +74,7 @@ Next, create a new page named `UserClaims.razor` and add the following code to d
```csharp title="UserClaims.razor"
@page "/user-claims"
@using System.Security.Claims
@using Microsoft.AspNetCore.Components.Authorization

<PageTitle>User Claims</PageTitle>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
template: templates/complete-guide.html
heading: Securing Routes within the app
read_time: 2 min
read_time: 5 min
---

In a .NET Blazor Web Application, handling and securing routes is an important aspect of managing user access, ensuring the right content is displayed to the right users, and protecting sensitive resources.
Expand All @@ -12,26 +12,36 @@ Blazor provides the `AuthorizeView` component to conditionally display UI elemen

Let’s first navigate to the `NavMenu.razor` file under the `/Components/Layout` directory and add the `AuthorizeView` component to encapsulate the `Counter` and `Weather` components. Then we can add the User Claims menu item as well.

```html title="NavMenu.razor"
<AuthorizeView>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>

<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>

<div class="nav-item px-3">
<NavLink class="nav-link" href="user-claims">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Claims
</NavLink>
</div>
</AuthorizeView>
```html title="NavMenu.razor" hl_lines="9-26"
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>

<AuthorizeView>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>

<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>

<div class="nav-item px-3">
<NavLink class="nav-link" href="user-claims">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Claims
</NavLink>
</div>
</AuthorizeView>
</nav>
</div>
```

We will be using the `Authorize` attribute to protect the routes for the above three pages so that only authenticated users can access them. If an unauthorized user attempts to access these routes, they should be redirected to the login page of the Identity Provider.
Expand All @@ -58,7 +68,9 @@ In order to redirect unauthorized users to the login page, we will create a Razo

Now, open the `Routes.razor` file and add the `AuthorizeRouteView` component as shown below, which is used to protect entire pages or routes. It only renders the associated route if the user is authorized. Otherwise, it can redirect the user to a login page using the `RedirectToLogin` component we created.

```html title="Routes.razor"
```html title="Routes.razor" hl_lines="5-9"
@using Microsoft.AspNetCore.Components.Authorization

<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
Expand All @@ -71,6 +83,12 @@ Now, open the `Routes.razor` file and add the `AuthorizeRouteView` component as
</Router>
```

Then navigate to the `Program.cs` file and add the following before the `app.Run();` method is invoked so that a RouteGroupBuilder will be created for the `/authentication` endpoint.

```csharp
app.MapGroup("/authentication").MapLoginAndLogout();
```

By following these steps, you can easily secure your Blazor application routes and manage user access based on their authentication state.

You can also start the application at this point and observe the homepage of the application with the changes we made.
Expand Down

0 comments on commit 6b4f6ba

Please sign in to comment.