06-authentication-and-authorization.md 26.8 KB
Newer Older
Steve Sanderson's avatar
Steve Sanderson committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Authentication

The application is working well. Users can place orders and track their order status. But there's one little problem: currently we don't distinguish between users at all. The "My orders" page lists *all* orders placed by *all* users, and anybody can view the state of anybody else's order. Your customers, and privacy regulations, may have an issue with this.

The solution is *authentication*. We need a way for users to log in, so we know who's who. Then we can implement *authorization*, which is to enforce rules about who's allowed to do what.

## Enforcement is on the server

The first and most important principle is that all *real* security rules must be enforced on the backend server. The client (UI) merely shows or hides options as a courtesy to well-behaved users, but a malicious user can always change the behavior of the client-side code.

As such, we're going to start by enforcing some access rules in the backend server, even before the client code knows about them.

Inside the `BlazorPizza.Server` project, you'll find `OrdersController.cs`. This is the controller class that handles incoming HTTP requests for `/orders` and `/orders/{orderId}`. To require that all requests to these endpoints come from authenticated users (i.e., people who have logged in), add the `[Authorize]` attribute to the `OrdersController` class:

```csharp
[Route("orders")]
[ApiController]
[Authorize]
public class OrdersController : Controller
{
}
```

24
25
The `AuthorizeAttribute` class is located in the `Microsoft.AspNetCore.Authorization` namespace.

26
If you try to run your application now, you'll find that you can no longer place orders, nor can you retrieve details of orders already placed. Requests to these endpoints will return HTTP 401 "Not Authorized" responses, triggering an error message in the UI. That's good, because it shows that rules are being enforced on the server!
Steve Sanderson's avatar
Steve Sanderson committed
27

28
![Secure orders](https://user-images.githubusercontent.com/1101362/83876158-49ffef80-a730-11ea-8c86-f1fb2b51755b.png)
Steve Sanderson's avatar
Steve Sanderson committed
29
30
31

## Tracking authentication state

32
The client code needs a way to track whether the user is logged in, and if so *which* user is logged in, so it can influence how the UI behaves. Blazor has a built-in DI service for doing this: the `AuthenticationStateProvider`. Blazor provides an implementation of the `AuthenticationStateProvider` service and other related services and components based on [OpenID Connect](https://openid.net/connect/) that handle all the details of establishing who the user is. These services and components are provided in the Microsoft.AspNetCore.Components.WebAssembly.Authentication package, which has already been added to the client project for you.
Steve Sanderson's avatar
Steve Sanderson committed
33

34
In broad terms, the authentication process implemented by these services looks like this:
Steve Sanderson's avatar
Steve Sanderson committed
35

36
37
38
39
40
41
42
* When a user attempts to login or tries to access a protected resource, the user is redirected to the app's login page (`/authentication/login`).
* In the login page, the app prepares to redirect to the authorization endpoint of the configured identity provider. The endpoint is responsible for determining whether the user is authenticated and for issuing one or more tokens in response. The app provides a login callback to receive the authentication response.
  * If the user isn't authenticated, the user is first redirected to the underlying authentication system (typically ASP.NET Core Identity).
  * Once the user is authenticated, the authorization endpoint generates the appropriate tokens and redirects the browser back to the login callback endpoint (`/authentication/login-callback`).
* When the Blazor WebAssembly app loads the login callback endpoint (`/authentication/login-callback`), the authentication response is processed.
  * If the authentication process completes successfully, the user is authenticated and optionally sent back to the original protected URL that the user requested.
  * If the authentication process fails for any reason, the user is sent to the login failed page (`/authentication/login-failed`), and an error is displayed.
Steve Sanderson's avatar
Steve Sanderson committed
43

44
See also [Secure ASP.NET Core Blazor WebAssembly](https://docs.microsoft.com/aspnet/core/security/blazor/webassembly/) for additional details.
Steve Sanderson's avatar
Steve Sanderson committed
45

46
To enable the authentication services, add a call to `AddApiAuthorization` in *Program.cs* in the client project:
Steve Sanderson's avatar
Steve Sanderson committed
47

48
```csharp
49
public static async Task Main(string[] args)
Steve Sanderson's avatar
Steve Sanderson committed
50
{
51
52
53
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    builder.RootComponents.Add<App>("app");

54
    builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
55
    builder.Services.AddScoped<OrderState>();
Steve Sanderson's avatar
Steve Sanderson committed
56
57

    // Add auth services
58
    builder.Services.AddApiAuthorization();
59
60

    await builder.Build().RunAsync();
Steve Sanderson's avatar
Steve Sanderson committed
61
62
63
}
```

64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
The added services will be configured by default to use an identity provider on the same origin as the app. The server project for the Blazing Pizza app has already been setup to use [IdentityServer](https://identityserver.io/) as the identity provider and ASP.NET Core Identity for the authentication system:

*BlazingPizza.Server/Startup.cs*

```csharp
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        .AddNewtonsoftJson();

    services.AddDbContext<PizzaStoreContext>(options => 
        options.UseSqlite("Data Source=pizza.db"));

    services.AddDefaultIdentity<PizzaStoreUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<PizzaStoreContext>();

    services.AddIdentityServer()
        .AddApiAuthorization<PizzaStoreUser, PizzaStoreContext>();

    services.AddAuthentication()
        .AddIdentityServerJwt();
}
```

The server has also already been configured to issue tokens to the client app:

*BlazingPizza.Server/appsettings.json*

```json
"IdentityServer": {
  "Clients": {
    "BlazingPizza.Client": {
      "Profile": "IdentityServerSPA"
    }
  }
}
```

To orchestrate the authentication flow, add an `Authentication` component to the *Pages* directory in the client project:

*BlazingPizza.Client/Pages/Authentication.razor*

```razor
@page "/authentication/{action}"

<RemoteAuthenticatorView Action="@Action" />

@code{
    [Parameter]
    public string Action { get; set; }
}
```

The `Authentication` component is setup to handle the various authentication actions using the built-in `RemoteAuthenticatorView` component. The `Action` parameter is bound to the `{action}` route value, which is then passed to the `RemoteAuthenticatorView` component to handle. The `RemoteAuthenticatorView` handles all of the actions used as part of remote authentication. Valid actions include: register, login, profile, and logout. See [Customize the authentication user interface](https://docs.microsoft.com/aspnet/core/security/blazor/webassembly/additional-scenarios#customize-app-routes) for more details.

Ryan Nowak's avatar
Ryan Nowak committed
119
To flow the authentication state information through your app, you need to add one more component. In `App.razor`, surround the entire `<Router>` with a `<CascadingAuthenticationState>`:
Steve Sanderson's avatar
Steve Sanderson committed
120
121
122

```html
<CascadingAuthenticationState>
Ryan Nowak's avatar
Ryan Nowak committed
123
124
    <Router AppAssembly="typeof(Program).Assembly" Context="routeData">
        ...
Steve Sanderson's avatar
Steve Sanderson committed
125
126
127
128
    </Router>
</CascadingAuthenticationState>
```

129
At first this will appear to do nothing, but in fact this has made a *cascading parameter* available to all descendant components. A cascading parameter is a parameter that isn't passed down just one level in the hierarchy, but through any number of levels.
Steve Sanderson's avatar
Steve Sanderson committed
130
131
132
133
134
135
136
137

Finally, you're ready to display something in the UI!

## Displaying login state

Create a new component called `LoginDisplay` in the client project's `Shared` folder, containing:

```html
138
139
140
@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

Steve Sanderson's avatar
Steve Sanderson committed
141
142
143
144
145
146
147
148
<div class="user-info">
    <AuthorizeView>
        <Authorizing>
            <text>...</text>
        </Authorizing>
        <Authorized>
            <img src="img/user.svg" />
            <div>
149
150
                <a href="authentication/profile" class="username">@context.User.Identity.Name</a>
                <button class="btn btn-link sign-out" @onclick="BeginSignOut">Sign out</button>
Steve Sanderson's avatar
Steve Sanderson committed
151
152
153
            </div>
        </Authorized>
        <NotAuthorized>
154
155
            <a class="sign-in" href="authentication/register">Register</a>
            <a class="sign-in" href="authentication/login">Log in</a>
Steve Sanderson's avatar
Steve Sanderson committed
156
157
158
        </NotAuthorized>
    </AuthorizeView>
</div>
159
160
161
162
163
164
165
166

@code{
    async Task BeginSignOut()
    {
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}
Steve Sanderson's avatar
Steve Sanderson committed
167
168
```

169
170
171
`AuthorizeView` is a built-in component that displays different content depending on whether the user meets specified authorization conditions. We didn't specify any authorization conditions, so by default it considers the user authorized if they are authenticated (logged in), otherwise not authorized.

You can use `AuthorizeView` anywhere you need UI content to vary by authorization state, such as controlling the visibility of menu entries based on a user's roles. In this case, we're using it to tell the user who they are, and conditionally show either a "log in" or "log out" link as applicable.
Steve Sanderson's avatar
Steve Sanderson committed
172

173
The links to register, log in, and see the user profile are normal links that navigate to the `Authentication` component. The sign out link is a button and has additional logic to prevent a forged request from logging the user out. Using a button ensures that the sign out can only be triggered by a user action, and the `SignOutSessionStateManager` service maintains state across the sign out flow to ensure the whole flow originated with a user action.
Steve Sanderson's avatar
Steve Sanderson committed
174
175
176
177
178
179
180
181
182
183
184

Let's put the `LoginDisplay` in the UI somewhere. Open `MainLayout`, and update the `<div class="top-bar">` as follows:

```html
<div class="top-bar">
    (... leave existing content in place ...)

    <LoginDisplay />
</div>
```

185
## Register a user and log in
Steve Sanderson's avatar
Steve Sanderson committed
186

187
Try it out now. Run the app and register a new user.
Steve Sanderson's avatar
Steve Sanderson committed
188

189
Select Register on the home page.
Steve Sanderson's avatar
Steve Sanderson committed
190

191
![Select register](https://user-images.githubusercontent.com/1874516/78322144-b25d0580-7522-11ea-863d-59083c2bf111.png)
Steve Sanderson's avatar
Steve Sanderson committed
192

193
Fill in an email address for the new user and a password.
Steve Sanderson's avatar
Steve Sanderson committed
194

195
![Register a new user](https://user-images.githubusercontent.com/1874516/78322197-e6d0c180-7522-11ea-8728-2bd9cbd3c8f8.png)
Steve Sanderson's avatar
Steve Sanderson committed
196

197
To compete the user registration, the user needs to confirm their email address. During development you can just click the link to confirm the account.
Steve Sanderson's avatar
Steve Sanderson committed
198

199
![Email confirmation](https://user-images.githubusercontent.com/1874516/78389880-62208a80-7598-11ea-945a-d2ced76133d9.png)
Steve Sanderson's avatar
Steve Sanderson committed
200

201
Once the user's email has been confirmed, select Login and enter the user's email address and password.
Steve Sanderson's avatar
Steve Sanderson committed
202

203
204
205
206
207
208
209
210
211
212
![Select login](https://user-images.githubusercontent.com/1874516/78389922-7bc1d200-7598-11ea-8a10-e8bf8efa512e.png)

![Login](https://user-images.githubusercontent.com/1874516/78390092-cc392f80-7598-11ea-9d8e-562c2be1aad6.png)

The user is logged in and redirected back to the home page.

![Logged in](https://user-images.githubusercontent.com/1874516/78390115-d9561e80-7598-11ea-912b-e9dd71f787f2.png)

## Request an access token

Daniel Roth's avatar
Daniel Roth committed
213
Even though you are now logged in, placing an order still fails because the HTTP request to place the order requires a valid access token. To request access tokens and attach them to outbound requests, use the `BaseAddressAuthorizationMessageHandler` with the `HttpClient` that you're using to make the request. This message handler will acquire access tokens using the built-in `IAccessTokenProvider` service and attach them to each request using the standard Authorization header. If an access token cannot be acquired, an `AccessTokenNotAvailableException` is thrown, which can be used to redirect the user to the login page to authorize a new token.
214

Daniel Roth's avatar
Daniel Roth committed
215
To add the `BaseAddressAuthorizationMessageHandler` to our `HttpClient` in our app, we'll use the [IHttpClientFactory` helpers from ASP.NET Core](https://docs.microsoft.com/aspnet/core/fundamentals/http-requests) with a strongly typed client.
216

Daniel Roth's avatar
Daniel Roth committed
217
To create the strongly typed client, add a new `OrdersClient` class to the client project. The class should take an `HttpClient` in its constructor, and provide methods getting and placing orders:
218

Daniel Roth's avatar
Daniel Roth committed
219
*BlazingPizza.Client/OrdersClient.cs*
220

Daniel Roth's avatar
Daniel Roth committed
221
222
223
224
225
226
227
228
229
230
231
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;

namespace BlazingPizza.Client
{
    public class OrdersClient
Steve Sanderson's avatar
Steve Sanderson committed
232
    {
Daniel Roth's avatar
Daniel Roth committed
233
        private readonly HttpClient httpClient;
Steve Sanderson's avatar
Steve Sanderson committed
234

Daniel Roth's avatar
Daniel Roth committed
235
        public OrdersClient(HttpClient httpClient)
236
        {
Daniel Roth's avatar
Daniel Roth committed
237
            this.httpClient = httpClient;
238
        }
Daniel Roth's avatar
Daniel Roth committed
239
240
241
242
243
244
245
246
247
248

        public async Task<IEnumerable<OrderWithStatus>> GetOrders() =>
            await httpClient.GetFromJsonAsync<IEnumerable<OrderWithStatus>>("orders");


        public async Task<OrderWithStatus> GetOrder(int orderId) =>
            await httpClient.GetFromJsonAsync<OrderWithStatus>($"orders/{orderId}");


        public async Task<int> PlaceOrder(Order order)
Steve Sanderson's avatar
Steve Sanderson committed
249
        {
Daniel Roth's avatar
Daniel Roth committed
250
251
252
253
            var response = await httpClient.PostAsJsonAsync("orders", order);
            response.EnsureSuccessStatusCode();
            var orderId = await response.Content.ReadFromJsonAsync<int>();
            return orderId;
Steve Sanderson's avatar
Steve Sanderson committed
254
        }
255
256
257
258
    }
}
```

Daniel Roth's avatar
Daniel Roth committed
259
Register the `OrdersClient` as a typed client, with the underlying `HttpClient` configured with the correct base address and the `BaseAddressAuthorizationMessageHandler`.
260

Daniel Roth's avatar
Daniel Roth committed
261
262
263
264
```csharp
builder.Services.AddHttpClient<OrdersClient>(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
```
265

Daniel Roth's avatar
Daniel Roth committed
266
Update each page where an `HttpClient` is used to manage orders to use the new typed `OrdersClient`. Inject an `OrdersClient` instead of an `HttpClient` and use the new client to make the API call. Wrap each call in a `try-catch` that handles exceptions of type `AccessTokenNotAvailableException` by calling the provided `Redirect()` method.
267

Daniel Roth's avatar
Daniel Roth committed
268
*Checkout.razor*
269

Daniel Roth's avatar
Daniel Roth committed
270
271
272
273
```csharp
async Task PlaceOrder()
{
    isSubmitting = true;
Steve Sanderson's avatar
Steve Sanderson committed
274

Daniel Roth's avatar
Daniel Roth committed
275
    try
276
    {
Daniel Roth's avatar
Daniel Roth committed
277
278
279
280
281
282
283
        var newOrderId = await OrdersClient.PlaceOrder(OrderState.Order);
        OrderState.ResetOrder();
        NavigationManager.NavigateTo($"myorders/{newOrderId}");
    }
    catch (AccessTokenNotAvailableException ex)
    {
        ex.Redirect();
284
285
286
287
    }
}
```

Daniel Roth's avatar
Daniel Roth committed
288
*MyOrders.razor*
Steve Sanderson's avatar
Steve Sanderson committed
289

Daniel Roth's avatar
Daniel Roth committed
290
291
292
293
294
295
296
297
298
299
300
301
302
```csharp
protected override async Task OnParametersSetAsync()
{
    try
    {
        ordersWithStatus = await OrdersClient.GetOrders();
    }
    catch (AccessTokenNotAvailableException ex)
    {
        ex.Redirect();
    }
}
```
303

Daniel Roth's avatar
Daniel Roth committed
304
*OrderDetails.razor*
305

Daniel Roth's avatar
Daniel Roth committed
306
307
308
309
310
311
```csharp
private async void PollForUpdates()
{
    invalidOrder = false;
    pollingCancellationToken = new CancellationTokenSource();
    while (!pollingCancellationToken.IsCancellationRequested)
312
    {
Daniel Roth's avatar
Daniel Roth committed
313
314
315
316
317
318
319
        try
        {
            orderWithStatus = await OrdersClient.GetOrder(OrderId);
            StateHasChanged();
            await Task.Delay(4000);
        }
        catch (AccessTokenNotAvailableException ex)
320
        {
Daniel Roth's avatar
Daniel Roth committed
321
322
            pollingCancellationToken.Cancel();
            ex.Redirect();
323
        }
Daniel Roth's avatar
Daniel Roth committed
324
        catch (Exception ex)
325
        {
Daniel Roth's avatar
Daniel Roth committed
326
327
328
329
            invalidOrder = true;
            pollingCancellationToken.Cancel();
            Console.Error.WriteLine(ex);
            StateHasChanged();
Steve Sanderson's avatar
Steve Sanderson committed
330
331
332
333
334
        }
    }
}
```

335
## Authorizing access to specific order details
Steve Sanderson's avatar
Steve Sanderson committed
336

337
Although the server requires authentication before accepting queries for order information, it still doesn't distinguish between users. All signed-in users can see the orders from all other signed-in users. We have authentication, but no authorization!
Steve Sanderson's avatar
Steve Sanderson committed
338

339
To verify this, place an order while signed in with one account. Then sign out and back in using a different account. You'll still be able to see the same order details.
Steve Sanderson's avatar
Steve Sanderson committed
340

341
This is easily fixed. Back in the `OrdersController` code, look for the commented-out line in `PlaceOrder`, and uncomment it:
Steve Sanderson's avatar
Steve Sanderson committed
342

343
344
345
```cs
order.UserId = GetUserId();
```
Steve Sanderson's avatar
Steve Sanderson committed
346

347
Now each order will be stamped with the ID of the user who owns it.
Steve Sanderson's avatar
Steve Sanderson committed
348

349
350
351
352
353
354
355
356
357
Next look for the commented-out `.Where` lines in `GetOrders` and `GetOrderWithStatus`, and uncomment both. These lines ensure that users can only retrieve details of their own orders:

```csharp
.Where(o => o.UserId == GetUserId())
```

Now if you run the app again, you'll no longer be able to see the existing order details, because they aren't associated with your user ID. If you place a new order with one account, you won't be able to see it from a different account. That makes the application much more useful.

## Ensure authentication before placing or viewing orders
Steve Sanderson's avatar
Steve Sanderson committed
358

359
Now if you're logged in, you'll be able to place orders and see order status. But if you log out then make another attempt to place an order, bad things will happen. The server will reject the `POST` request, causing a client-side exception, but the user won't know why.
Steve Sanderson's avatar
Steve Sanderson committed
360

361
To fix this on the checkout page, let's make the UI prompt the user to log in (if necessary) as part of placing an order.
Steve Sanderson's avatar
Steve Sanderson committed
362

Ryan Nowak's avatar
Ryan Nowak committed
363
In the `Checkout` page component, add an `OnInitializedAsync` with some logic to to check whether the user is currently authenticated. If they aren't, send them off to the login endpoint.
Steve Sanderson's avatar
Steve Sanderson committed
364
365

```cs
366
@code {
367
    [CascadingParameter] public Task<AuthenticationState> AuthenticationStateTask { get; set; }
Steve Sanderson's avatar
Steve Sanderson committed
368

369
    protected override async Task OnInitializedAsync()
Steve Sanderson's avatar
Steve Sanderson committed
370
371
372
373
374
375
    {
        var authState = await AuthenticationStateTask;
        if (!authState.User.Identity.IsAuthenticated)
        {
            // The server won't accept orders from unauthenticated users, so avoid
            // an error by making them log in at this point
376
            NavigationManager.NavigateTo("authentication/login?redirectUri=/checkout", true);
Steve Sanderson's avatar
Steve Sanderson committed
377
378
379
380
381
382
383
        }
    }

    // Leave PlaceOrder unchanged here
}
```

384
Try it out: now if you're logged out and get to the checkout screen, you'll be redirected to log in. The value for the `[CascadingParameter]` comes from your `AuthenticationStateProvider` via the `CascadingAuthenticationState` you added earlier.
Steve Sanderson's avatar
Steve Sanderson committed
385

386
But do you notice something a bit awkward about it? It still shows the checkout UI briefly before the browser loads the login page. We could fix that  by wrapping the "checkout" UI inside an `AuthorizeView` like we did in the `LoginDisplay`. But there's an even easier way to ensure that anyone who navigates to the checkout page is logged in. We can enforce that the entire page requires authentication using the router.
Steve Sanderson's avatar
Steve Sanderson committed
387

388
To set this up, update *App.razor* to render an `AuthorizeRouteView` instead of a `RouteView` when the route is found.
Steve Sanderson's avatar
Steve Sanderson committed
389

390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
```razor
<CascadingAuthenticationState>
    <Router AppAssembly="typeof(Program).Assembly" Context="routeData">
        <Found>
            <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)">
                <NotAuthorized>
                    <p>You are not authorized to access this resource.</p>
                </NotAuthorized>
                <Authorizing>
                    <div class="main">Please wait...</div>
                </Authorizing>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="typeof(MainLayout)">
                <div class="main">Sorry, there's nothing at this address.</div>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>
```
Steve Sanderson's avatar
Steve Sanderson committed
411

412
The `AuthorizeRouteView` will route navigations to the correct component, but only if the user is authorized. If the user is not authorized, the `NotAuthorized` content is displayed. You can also specify content to display while the `AuthorizeRouteView` is determining if the user is authorized.
Steve Sanderson's avatar
Steve Sanderson committed
413

414
By default, all pages allow for anonymous access, but we can specify that the user must be logged in to access the checkout page by adding the `[Authorize]` attribute. You add attributes to a component using the `@attribute` directive.
Steve Sanderson's avatar
Steve Sanderson committed
415

416
Update the `Checkout`, `MyOrders`, and `OrderDetails` pages to add the `[Authorize]` attribute;
Steve Sanderson's avatar
Steve Sanderson committed
417

418
419
```razor
@attribute [Authorize]
Steve Sanderson's avatar
Steve Sanderson committed
420
421
```

422
Now when you try to nativgate to any of these pages while signed out, you see the `NotAuthorized` content we setup in *App.razor*.
Steve Sanderson's avatar
Steve Sanderson committed
423

424
![Not authorized](https://user-images.githubusercontent.com/1874516/78410504-63b27880-75c1-11ea-8c2c-ab62c1c24596.png)
Steve Sanderson's avatar
Steve Sanderson committed
425

426
Instead of telling the user they are unauthorized it would be better if we redirected them to the login page. To do that, add the following `RedirectToLogin` component:
Steve Sanderson's avatar
Steve Sanderson committed
427

428
*BlazingPizza.Client/Shared/RedirectToLogin.razor*
Steve Sanderson's avatar
Steve Sanderson committed
429

430
431
432
433
```razor
@inject NavigationManager Navigation
@code {
    protected override void OnInitialized()
Steve Sanderson's avatar
Steve Sanderson committed
434
    {
435
        Navigation.NavigateTo($"authentication/login?returnUrl={Navigation.Uri}");
Steve Sanderson's avatar
Steve Sanderson committed
436
437
438
439
    }
}
```

440
Then replace the `NotAuthorized` content in *App.razor* with the `RedirectToLogin` component.
Steve Sanderson's avatar
Steve Sanderson committed
441

442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
```razor
<CascadingAuthenticationState>
    <Router AppAssembly="typeof(Program).Assembly" Context="routeData">
        <Found>
            <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)">
                <NotAuthorized>
                    <RedirectToLogin />
                </NotAuthorized>
                <Authorizing>
                    <div class="main">Please wait...</div>
                </Authorizing>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="typeof(MainLayout)">
                <div class="main">Sorry, there's nothing at this address.</div>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>
Steve Sanderson's avatar
Steve Sanderson committed
462
463
```

464
If you now try to access the "myorders" page while signed out, you are redirected to the login page. And once the user is logged in, they are redirected back to the page they were trying to access thanks to the `returnUrl` parameter.
Steve Sanderson's avatar
Steve Sanderson committed
465

466
It's a bit unfortunate that users can see the My Orders tab when they are not logged in. We can hide the My Orders tab for unauthenticated users using the `AuthorizeView` component.
Steve Sanderson's avatar
Steve Sanderson committed
467

468
Update `MainLayout` to wrap the My Orders `NavLink` in an `AuthorizeView`.
Steve Sanderson's avatar
Steve Sanderson committed
469

470
471
472
473
474
475
476
477
```razor
<AuthorizeView>
    <NavLink href="myorders" class="nav-tab">
        <img src="img/bike.svg" />
        <div>My Orders</div>
    </NavLink>
</AuthorizeView>
```
Steve Sanderson's avatar
Steve Sanderson committed
478

479
The My Orders tab should now only be visible when the user is logged in.
Steve Sanderson's avatar
Steve Sanderson committed
480

481
We've now seen that there are three basic ways to interact with the authentication/authorization system inside components:
Steve Sanderson's avatar
Steve Sanderson committed
482

483
 * Receive a `Task<AuthenticationState>` as a cascading parameter. This is useful when you want to use the `AuthenticationState` in procedural logic such as an event handler.
484
485
 * Wrap content in an `AuthorizeView`. This is useful when you just need to vary some UI content according to authorization status.
 * Place an `[Authorize]` attribute on a routable component. This is useful if you want to control the reachability of an entire page based on authorization conditions.
Steve Sanderson's avatar
Steve Sanderson committed
486

487
## Preserving order state across the redirection flow
Steve Sanderson's avatar
Steve Sanderson committed
488

489
We've just introduced a pretty serious defect into the application. Since you're building a client-side SPA, the application state (such as the current order) is held in the browser's memory. When you redirect away to log in, that state is discarded. When the user is redirected back, their order has now become empty!
Steve Sanderson's avatar
Steve Sanderson committed
490

491
Check you can reproduce this bug. Start logged out, and create an order. When you try to place the order, you get redirected to the login page. After logging in, you are then redirected to the checkout page, but your pizzas in your order have now gone missing! This is a common concern with browser-based single-page applications (SPAs), but fortunately there is a straightforward solution.
Steve Sanderson's avatar
Steve Sanderson committed
492

493
494
495
496
497
498
499
500
501
We'll fix the bug by persisting the order state. Blazor's authentication library makes this straight forward to do.

To define the state that we want persisted, add a `PizzaAuthenticationState` class that inherits from `RemoteAuthenticationState`. `RemoteAuthenticationState` is used by the authentication system to preserve state across the redirects, like the return URL. When you derive from this type, any public properties will be JSON serialized as part of the persisted state. Add an `Order` property to persist the current order.

```csharp
public class PizzaAuthenticationState : RemoteAuthenticationState
{
    public Order Order { get; set; }
}
Steve Sanderson's avatar
Steve Sanderson committed
502
503
```

504
505
506
507
To configure the authentication system to use our `PizzaAuthenticationState` instead of the default `RemoteAuthenticationState`, update *Program.cs* as follows:

```csharp
// Add auth services
508
builder.Services.AddApiAuthorization<PizzaAuthenticationState>();
509
```
Ryan Nowak's avatar
Ryan Nowak committed
510

511
Now we need to add logic to persist the current order, and then reestablish the current order from the persisted state after the user has successfully logged in. To do that, update the `Authentication` component to use `RemoteAuthenticatorViewCore` instead of `RemoteAuthenticatorView`. Override `OnInitialized` to setup the order state to be persisted, and implement the `OnLogInSucceeded` callback to reestablish the order state. You'll need to add a `ReplaceOrder` method to `OrderState` so that you can reestablish the saved order.
Ryan Nowak's avatar
Ryan Nowak committed
512

513
*BlazingPizza.Client/Pages/Authentication.razor*
Ryan Nowak's avatar
Ryan Nowak committed
514

515
516
517
518
```razor
@page "/authentication/{action}"
@inject OrderState OrderState
@inject NavigationManager NavigationManager
Ryan Nowak's avatar
Ryan Nowak committed
519

520
521
522
523
524
<RemoteAuthenticatorViewCore
    TAuthenticationState="PizzaAuthenticationState"
    AuthenticationState="RemoteAuthenticationState"
    OnLogInSucceeded="RestorePizza"
    Action="@Action" />
Ryan Nowak's avatar
Ryan Nowak committed
525

526
527
@code{
    [Parameter] public string Action { get; set; }
Steve Sanderson's avatar
Steve Sanderson committed
528

529
    public PizzaAuthenticationState RemoteAuthenticationState { get; set; } = new PizzaAuthenticationState();
Steve Sanderson's avatar
Steve Sanderson committed
530

531
532
533
534
535
536
537
538
    protected override void OnInitialized()
    {
        if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn, Action))
        {
            // Preserve the current order so that we don't loose it
            RemoteAuthenticationState.Order = OrderState.Order;
        }
    }
Steve Sanderson's avatar
Steve Sanderson committed
539

540
541
542
543
544
545
546
547
548
    private void RestorePizza(PizzaAuthenticationState pizzaState)
    {
        if (pizzaState.Order != null)
        {
            OrderState.ReplaceOrder(pizzaState.Order);
        }
    }
}
```
Steve Sanderson's avatar
Steve Sanderson committed
549

550
*BlazingPizza.Client/OrderState.cs*
Steve Sanderson's avatar
Steve Sanderson committed
551

552
553
554
555
```csharp
public class OrderState
{
    ...
Steve Sanderson's avatar
Steve Sanderson committed
556

557
558
559
560
561
562
    public void ReplaceOrder(Order order)
    {
        Order = order;
    }
}
```
Steve Sanderson's avatar
Steve Sanderson committed
563

564
Now if you try to place an order when signed out, you can see the order persisted in local storage during the authentication process:
Steve Sanderson's avatar
Steve Sanderson committed
565

566
![Persisted order state](https://user-images.githubusercontent.com/1874516/78414685-30c4b080-75d2-11ea-98df-d1ac73548774.png)
Steve Sanderson's avatar
Steve Sanderson committed
567

568
## Customize the logout experience
Steve Sanderson's avatar
Steve Sanderson committed
569

570
Currently, when the user signs out, they are brought to a generic logged out page:
Steve Sanderson's avatar
Steve Sanderson committed
571

572
![Logged out](https://user-images.githubusercontent.com/1874516/78414080-4684a680-75cf-11ea-808d-8d44a5f3941e.png)
Steve Sanderson's avatar
Steve Sanderson committed
573

574
You can customize this page in your `Authentication` component by setting the `LogOutSucceeded` property on `RemoteAuthenticatorViewCore`.
Steve Sanderson's avatar
Steve Sanderson committed
575

576
But what if we want the user to be redirected back to the home page after they log out? To do that, we can configure in *Program.cs* which path to direct the user to when they successfully log out.
Steve Sanderson's avatar
Steve Sanderson committed
577
578

```csharp
579
// Add auth services
580
builder.Services.AddApiAuthorization<PizzaAuthenticationState>(options =>
581
582
583
{
    options.AuthenticationPaths.LogOutSucceededPath = "";
});
Steve Sanderson's avatar
Steve Sanderson committed
584
585
```

586
Now when you sign out, the user should be brought back to the home page.
Steve Sanderson's avatar
Steve Sanderson committed
587
588

Next up - [JavaScript interop](07-javascript-interop.md)