UPDATED - After discussing with Matt G, I added a better explanation to my answer in order to be clear on my point. I reckon I wasn't clear enough at the beginning.
UPDATE 2 - Adding point 5
I think that a token should be issued for one client and must be used only by that specific client to access all the resources it asked access for.
Case
- Api1 asks for a token and can access Api2, Api3, Api4, Api5.
- Api2 uses Api1's token and have the access to the same resources as Api1.
Comments
It means that Api2 can access Api3, Api4, Api5. But what happens if Api2 shouldn't be granted access for Api5? Now you have problems. As soon as this situation show up, you have to redesign your security mechanism.
In addition, it means that the token sent to an Api2 contains scopes that are not relevant to it which sounds like a bit strange for me.
In the other hand, a scope for Api1 may mean something different for Api2 which can lead to misunderstandings. But this will depend on your development.
If you do authentication and authorization using Scopes you shouldn't be sharing your token because Api1 can execute code that for example Api2 shouldn't execute and this is a security issue.
If Api1 is the one that asks for the token to the IdP. What happen to Api2 if
you want to used it separately from Api1? it can't do calls to others Apis because Api1 did not pass it the token? Or all the Apis have the ability to ask for tokens to the IdP and all of them pass the token through to the others Apis depending on which Api did the first call? Are you probably putting more complexity than need it?
What you are trying to achieve is doable but for me it's not a good idea.
Below I propose you an alternative solution to this problem.
It sounds like you need a TokenCache and a mechanism to inject it every time you do HttpClient.Send. This is what I propose you.
You should create a class called TokenCache, this class is responsible for getting the Token each time is expired, invalid or null.
public class TokenCache : ITokenCache
{
public TokenClient TokenClient { get; set; }
private readonly string _scope;
private DateTime _tokenCreation;
private TokenResponse _tokenResponse;
public TokenCache(string scope)
{
_scope = scope;
}
private bool IsTokenValid()
{
return _tokenResponse != null &&
!_tokenResponse.IsError &&
!string.IsNullOrWhiteSpace(_tokenResponse.AccessToken) &&
(_tokenCreation.AddSeconds(_tokenResponse.ExpiresIn) > DateTime.UtcNow);
}
private async Task RequestToken()
{
_tokenResponse = await TokenClient.RequestClientCredentialsAsync(_scope).ConfigureAwait(false);
_tokenCreation = DateTime.UtcNow;
}
public async Task<string> GetAccessToken(bool forceRefresh = false)
{
if (!forceRefresh && IsTokenValid()) return _tokenResponse.AccessToken;
await RequestToken().ConfigureAwait(false);
if (!IsTokenValid())
{
throw new InvalidOperationException("An unexpected token validation error has occured during a token request.");
}
return _tokenResponse.AccessToken;
}
}
You create a class TokenHttpHandler as shown below. This class is going to set the Bearer token each time you do a HttpClient.Send. Notice that we are using TokenCache (_tokenCache.GetAccessToken) to get the token inside SetAuthHeaderAndSendAsync method. This way you know for sure that your token is going to be sent each time you do calls from your api/mvc app to another api.
public class TokenHttpHandler : DelegatingHandler
{
private readonly ITokenCache _tokenCache;
public TokenHttpHandler(ITokenCache tokenCache)
{
InnerHandler = new HttpClientHandler();
_tokenCache = tokenCache;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await SetAuthHeaderAndSendAsync(request, cancellationToken, false).ConfigureAwait(false);
//check for 401 and retry
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
response = await SetAuthHeaderAndSendAsync(request, cancellationToken, true);
}
return response;
}
private async Task<HttpResponseMessage> SetAuthHeaderAndSendAsync(HttpRequestMessage request, CancellationToken cancellationToken, bool forceTokenRefresh)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await _tokenCache.GetAccessToken(forceTokenRefresh).ConfigureAwait(false));
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
Then you use it inside the ExtendedHttpClient as shown below. Notice that we are Injecting the TokenHttpHandler into the constructor.
public class ExtendedHttpClient : HttpClient
{
public ExtendedHttpClient(TokenHttpHandler messageHandler) : base(messageHandler)
{
DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
}
}
And finally in your IoC configuration y you need to add the new classes.
If you want to reuse the above code for multiple MVC apps/Api, so you should put it in a shared library (for example infrastructure) and then only configure the IoC for each IdentityServer client.
builder.RegisterType<TokenHttpHandler>().AsSelf();
builder.RegisterType<ExtendedHttpClient>().As<HttpClient>();
builder.RegisterType<TokenCache>()
.As<ITokenCache>()
.WithParameter("scope", "YOUR_SCOPES")
.OnActivating(e => e.Instance.TokenClient = e.Context.Resolve<TokenClient>())
.SingleInstance();
builder.Register(context =>
{
var address = "YOUR_AUTHORITY";
return new TokenClient(address, "ClientID", "Secret");
})
.AsSelf();
Notice that this examples uses ClientCredentials flow but you can take this concept and modify it to make it fit with your requirements.
Hope it helps.
Kind regards
Daniel