/
AuthorizationMessageHandler.cs
124 lines (111 loc) · 5.51 KB
/
AuthorizationMessageHandler.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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
119
120
121
122
123
124
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
{
/// <summary>
/// A <see cref="DelegatingHandler"/> that attaches access tokens to outgoing <see cref="HttpResponseMessage"/> instances.
/// Access tokens will only be added when the request URI is within one of the base addresses configured using
/// <see cref="ConfigureHandler(IEnumerable{string}, IEnumerable{string}, string)"/>.
/// </summary>
public class AuthorizationMessageHandler : DelegatingHandler
{
private readonly IAccessTokenProvider _provider;
private readonly NavigationManager _navigation;
private AccessToken _lastToken;
private AuthenticationHeaderValue _cachedHeader;
private Uri[] _authorizedUris;
private AccessTokenRequestOptions _tokenOptions;
/// <summary>
/// Initializes a new instance of <see cref="AuthorizationMessageHandler"/>.
/// </summary>
/// <param name="provider">The <see cref="IAccessTokenProvider"/> to use for provisioning tokens.</param>
/// <param name="navigation">The <see cref="NavigationManager"/> to use for performing redirections.</param>
public AuthorizationMessageHandler(
IAccessTokenProvider provider,
NavigationManager navigation)
{
_provider = provider;
_navigation = navigation;
}
/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var now = DateTimeOffset.Now;
if (_authorizedUris == null)
{
throw new InvalidOperationException($"The '{nameof(AuthorizationMessageHandler)}' is not configured. " +
$"Call '{nameof(AuthorizationMessageHandler.ConfigureHandler)}' and provide a list of endpoint urls to attach the token to.");
}
if (_authorizedUris.Any(uri => uri.IsBaseOf(request.RequestUri)))
{
if (_lastToken == null || now >= _lastToken.Expires.AddMinutes(-5))
{
var tokenResult = _tokenOptions != null ?
await _provider.RequestAccessToken(_tokenOptions) :
await _provider.RequestAccessToken();
if (tokenResult.TryGetToken(out var token))
{
_lastToken = token;
_cachedHeader = new AuthenticationHeaderValue("Bearer", _lastToken.Value);
}
else
{
throw new AccessTokenNotAvailableException(_navigation, tokenResult, _tokenOptions?.Scopes);
}
}
// We don't try to handle 401s and retry the request with a new token automatically since that would mean we need to copy the request
// headers and buffer the body and we expect that the user instead handles the 401s. (Also, we can't really handle all 401s as we might
// not be able to provision a token without user interaction).
request.Headers.Authorization = _cachedHeader;
}
return await base.SendAsync(request, cancellationToken);
}
/// <summary>
/// Configures this handler to authorize outbound HTTP requests using an access token. The access token is only attached if at least one of
/// <paramref name="authorizedUrls" /> is a base of <see cref="HttpRequestMessage.RequestUri" />.
/// </summary>
/// <param name="authorizedUrls">The base addresses of endpoint URLs to which the token will be attached.</param>
/// <param name="scopes">The list of scopes to use when requesting an access token.</param>
/// <param name="returnUrl">The return URL to use in case there is an issue provisioning the token and a redirection to the
/// identity provider is necessary.
/// </param>
/// <returns>This <see cref="AuthorizationMessageHandler"/>.</returns>
public AuthorizationMessageHandler ConfigureHandler(
IEnumerable<string> authorizedUrls,
IEnumerable<string> scopes = null,
string returnUrl = null)
{
if (_authorizedUris != null)
{
throw new InvalidOperationException("Handler already configured.");
}
if (authorizedUrls == null)
{
throw new ArgumentNullException(nameof(authorizedUrls));
}
var uris = authorizedUrls.Select(uri => new Uri(uri, UriKind.Absolute)).ToArray();
if (uris.Length == 0)
{
throw new ArgumentException("At least one URL must be configured.", nameof(authorizedUrls));
}
_authorizedUris = uris;
var scopesList = scopes?.ToArray();
if (scopesList != null || returnUrl != null)
{
_tokenOptions = new AccessTokenRequestOptions
{
Scopes = scopesList,
ReturnUrl = returnUrl
};
}
return this;
}
}
}