Async method in lock-statement block - c#

I want to place an authentication token in a cache where it can be used in multiple app domains. This token will expire every hour. When a client is unable to authorize with the token, it asks the token generation service to generate a new one.
I only want this regeneration to occur for the first client that does not authenticate sucessfully, so I've employed a lock object like this:
public async Task<Token> GenerateToken(Token oldToken)
{
Token token;
lock (lockObject)
{
var cachedToken = GetTokenFromCache();
if (cachedToken == oldToken)
{
var authClient = new AuthClient(id, key);
token = await authClient.AuthenticateClientAsync(); //KABOOM
PutTokenInCache(token);
}
else
{
token = cachedToken;
}
}
return token;
}
My issue is AuthClient only has async methods, and async methods are not allowed in lock-statement blocks. I don't have any control over AuthClient, is there some other strategy I can employ here?

You can use SemaphoreSlim as a basic async-ready lock replacement:
private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1);
public async Task<Token> GenerateToken(Token oldToken)
{
Token token;
await lockObject.WaitAsync();
try
{
var cachedToken = GetTokenFromCache();
if (cachedToken == oldToken)
{
var authClient = new AuthClient(id, key);
token = await authClient.AuthenticateClientAsync();
PutTokenInCache(token);
}
else
{
token = cachedToken;
}
}
finally
{
lockObject.Release();
}
return token;
}

Related

AcquireTokenSilentAsync never using tokens from the cache

I have a class that acts as a wrapper for the MS Graph SDK. Fairly simple purpose, inside the class there are methods for getting various data sets out of Graph for a particular user.
EDIT: this runs under the context of an application, so no user creds are ever used.
All of that part works fine, what isn't working is the DelegateAuthenticationProvider never finds the access token in the cache. Each call to a graph endpoint gets a new token, even in the same instance of the class. Within the class I'm using a singleton pattern for the GraphServiceClient.
Here is the code I'm using to handle the client:
private static GraphServiceClient _graphServiceClient;
private static AuthenticationContext _authContext;
private static readonly object _locker = new();
private GraphServiceClient GetClient(M365ServiceOptions options)
{
if (_graphServiceClient == null)
{
lock (_locker)
{
if (_graphServiceClient == null)
{
_authContext = new AuthenticationContext($"https://login.microsoftonline.com/{options.TenantId}/");
var provider = new DelegateAuthenticationProvider(async (requestMessage) =>
{
AuthenticationResult accessToken;
try
{
//Use Token from cache or refresh token
accessToken = await _authContext.AcquireTokenSilentAsync(options.GraphURL, options.ClientId);
_logger.LogDebug("Cache Hit");
}
catch (AdalSilentTokenAcquisitionException)
{
//If no cached token, get a new one
_logger.LogDebug($"Cache Miss: {_authContext.TokenCache?.Count}");
var credentials = new ClientCredential(options.ClientId, options.ClientSecret);
accessToken = _authContext.AcquireTokenAsync(options.GraphURL, credentials).Result;
}
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken);
});
_graphServiceClient = new GraphServiceClient(provider);
}
}
}
return _graphServiceClient;
}
While debugging it is clear the token cache has an item in it, and the details all seem to match, but no matter what, the AcquireTokenSilentAsync always throws the AdalSilentTokenAcquisitionException exception and forces it to get a new token for each call. This is impacting performance as no matter what, each call to the graph gets a new token.
Thank you for any assistance.
Please try this class. First call AuthenticationHelper.GetAuthenticatedClient() to get the a GraphServiceClient, then you this to access the user information.
public class AuthenticationHelper
{
static readonly string clientId = "";
public static string[] Scopes = { "User.Read" };
public static PublicClientApplication IdentityClientApp = new PublicClientApplication(clientId);
public static string TokenForUser = null;
public static DateTimeOffset Expiration;
private static GraphServiceClient graphClient = null;
// Get an access token for the given context and resourced. An attempt is first made to
// acquire the token silently. If that fails, then we try to acquire the token by prompting the user.
public static GraphServiceClient GetAuthenticatedClient()
{
if (graphClient == null)
{
// Create Microsoft Graph client.
try
{
graphClient = new GraphServiceClient(
"https://graph.microsoft.com/v1.0",
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
var token = await GetTokenForUserAsync();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
requestMessage.Headers.Add("SampleID", "MSGraphConsoleApp");
}));
return graphClient;
}
catch (Exception ex)
{
Debug.WriteLine("Could not create a graph client: " + ex.Message);
}
}
return graphClient;
}
public static async Task<string> GetTokenForUserAsync()
{
AuthenticationResult authResult;
try
{
authResult = await IdentityClientApp.AcquireTokenSilentAsync(Scopes, IdentityClientApp.GetAccountsAsync().Result.First());
TokenForUser = authResult.AccessToken;
}
catch (Exception)
{
if (TokenForUser == null || Expiration <= DateTimeOffset.UtcNow.AddMinutes(5))
{
authResult = await IdentityClientApp.AcquireTokenAsync(Scopes);
TokenForUser = authResult.AccessToken;
Expiration = authResult.ExpiresOn;
}
}
return TokenForUser;
}
public static void SignOut()
{
foreach (var user in IdentityClientApp.GetAccountsAsync().Result)
{
IdentityClientApp.RemoveAsync(user);
}
graphClient = null;
TokenForUser = null;
}
}

Implementing short lived Jwt with Refresh Token with Blazor

We are currently developing a Blazor app which is secured using short lived (10 minute) Jwt with Refresh Tokens.
Currently we have the Jwt implemented and through the Blazor server side web api can login, generate the Jwt and generate the refresh token.
From the client side I have used the following link;
Authentication With client-side Blazor
and extended the ApiAuthenticationStateProvider.cs as follows;
public class ApiAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly HttpClient _httpClient;
private readonly ILocalStorageService _localStorage;
public ApiAuthenticationStateProvider(HttpClient httpClient, ILocalStorageService localStorage)
{
_httpClient = httpClient;
_localStorage = localStorage;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var savedToken = await _localStorage.GetItemAsync<string>("authToken");
var refreshToken = await _localStorage.GetItemAsync<string>("refreshToken");
if (string.IsNullOrWhiteSpace(savedToken) || string.IsNullOrWhiteSpace(refreshToken))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
var userResponse = await _httpClient.GetAsync<UserModel>("api/accounts/user", savedToken);
if(userResponse.HasError)
{
var response = await _httpClient.PostAsync<LoginResponse>("api/login/refreshToken", new RefreshTokenModel { RefreshToken = refreshToken });
//check result now
if (!response.HasError)
{
await _localStorage.SetItemAsync("authToken", response.Result.AccessToken);
await _localStorage.SetItemAsync("refreshToken", response.Result.RefreshToken);
userResponse = await _httpClient.GetAsync<UserModel>("api/accounts/user", response.Result.AccessToken);
}
}
var identity = !userResponse.HasError ? new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, userResponse.Result.Email) }, "apiauth") : new ClaimsIdentity();
return new AuthenticationState(new ClaimsPrincipal(identity));
}
public void MarkUserAsAuthenticated(string email)
{
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, email) }, "apiauth"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}
public void MarkUserAsLoggedOut()
{
var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
var authState = Task.FromResult(new AuthenticationState(anonymousUser));
NotifyAuthenticationStateChanged(authState);
}
}
So if the Jwt fails the first time we try to renew with the refresh token.
The code above is working, however the first issue i found is, if I then navigate to the /fetchData test end point (which is protected with the [Authorize] attribute). The page initially runs fine and sends the Jwt in the header. However, if i then f5 and refresh the page I get a 401 unauthorized on the /fecthData endpoint, i.e. on the code;
#code {
WeatherForecast[] forecasts;
protected override async Task OnInitAsync()
{
forecasts = await Http.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");
}
}
Now if to get around this I can manually add the Jwt form localStorage to the header (in my case I use an extension method);
public static async Task<ServiceResponse<T>> GetAsync<T>(
this HttpClient httpClient, string url, string token)
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
var response = await httpClient.GetAsync(url);
return await BuildResponse<T>(response);
}
However, the second issue I have here is that if the Jwt expires during this call I would need to call to use the refresh token to get a new Jwt.
Is there a way I can do this do this with middleware to avoid having to check for a 401 on each call and then renewing the token this way?
So often, we are thinking on Blazor as an MVC but it is not. It's more like a desktop app running inside browser. I use JWT and renewing tokens in this way: after login, I have an infinite loop that is pinging backend and keeping the session and renewing the tokens. Simplifying:
class JWTAuthenticationStateProvider : AuthenticationStateProvider
{
private bool IsLogedIn = false;
private CustomCredentials credentials = null;
// private ClaimsPrincipal currentClaimsPrincipal = null; (optinally)
public Task Login( string user, string password )
{
credentials = go_backend_login_service( user, password );
// do stuff with credentials and claims
// I raise event here to notify login
keepSession( );
}
public Task Logout( )
{
go_bakcend_logout_service( credentials );
// do stuff with claims
IsLogedIn = false;
// I raise event here to notify logout
}
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
// make a response from credentials or currentClaimsPrincipal
}
private async void KeepSession()
{
while(IsLogedIn)
{
credentials = go_backend_renewingJWT_service( credentials );
// do stuff with new credentials: check are ok, update IsLogedIn, ...
// I raise event here if server says logout
await Task.Delay(1000); // sleep for a while.
}
}
}
Remember to register component by DI:
public void ConfigureServices(IServiceCollection services)
{
// ... other services added here ...
// One JWTAuthenticationStateProvider for each connection on server side.
// A singleton for clientside.
services.AddScoped<AuthenticationStateProvider,
JWTAuthenticationStateProvider>();
}
This is just one idea, you should to think about it and adapt it to your own solution.
More about Authentication and Authorization on github SteveSandersonMS/blazor-auth.md

AD authentication token is not getting same for every user in bot framework V4 web chat

I am using Bot Framework V4, AD Authentication for our bot is working fine.But when ever i am trying to user new session it is taking the same Token by which it is logged previously. So I am getting the same data in all the sessions. I am using AuthenticationDialog provided by Enterprise Bot Template
Actual: I am getting logged in once and it is staying logged in all sessions(even in other machines)
Expected: I expect every session should take me to the sign in card(OAurth card)
public class AuthenticationDialog : ComponentDialog
{
private static AuthenticationResponses _responder = new AuthenticationResponses();
public AuthenticationDialog(string connectionName)
: base(nameof(AuthenticationDialog))
{
InitialDialogId = nameof(AuthenticationDialog);
ConnectionName = connectionName;
var authenticate = new WaterfallStep[]
{
PromptToLogin,
FinishLoginDialog,
};
AddDialog(new WaterfallDialog(InitialDialogId, authenticate));
AddDialog(new OAuthPrompt(DialogIds.LoginPrompt, new OAuthPromptSettings()
{
ConnectionName = ConnectionName,
Title = AuthenticationStrings.TITLE,
Text = AuthenticationStrings.PROMPT,
}));
}
private string ConnectionName { get; set; }
private async Task<DialogTurnResult> PromptToLogin(WaterfallStepContext sc, CancellationToken cancellationToken)
{
return await sc.PromptAsync(AuthenticationResponses.ResponseIds.LoginPrompt, new PromptOptions());
}
private async Task<DialogTurnResult> FinishLoginDialog(WaterfallStepContext sc, CancellationToken cancellationToken)
{
var activity = sc.Context.Activity;
if (sc.Result != null)
{
var tokenResponse = sc.Result as TokenResponse;
if (tokenResponse?.Token != null)
{
var user = await GetProfile(sc.Context, tokenResponse);
await _responder.ReplyWith(sc.Context, AuthenticationResponses.ResponseIds.SucceededMessage, new { name = user.DisplayName });
return await sc.EndDialogAsync(tokenResponse);
}
}
else
{
await _responder.ReplyWith(sc.Context, AuthenticationResponses.ResponseIds.FailedMessage);
}
return await sc.EndDialogAsync();
}
private async Task<User> GetProfile(ITurnContext context, TokenResponse tokenResponse)
{
var token = tokenResponse;
var client = new GraphClient(token.Token);
return await client.GetMe();
}
private class DialogIds
{
public const string LoginPrompt = "loginPrompt";
}
}
This is a known issue in WebChat. When you use the same user id for every conversation, the conversation will reference the same data stores. To resolve this issue, I would recommend generating random user ids for each conversation.
Hope this helps.

What pattern to use for init value

I have a service, which works with external resource (Microsoft Graph):
public class Office365DomainService : IOffice365DomainService
{
private GraphServiceClient _graphClient;
public async Task AddDomainAsync(string domain)
{
await _graphClient.Domains.Request().AddAsync(new Microsoft.Graph.Domain { Id = domain });
}
public async Task<string> GetMxRecordForDomainAsync(string domain)
{
var collection = await _graphClient.Domains[domain].ServiceConfigurationRecords.Request().GetAsync();
return String.Empty;
}
public async Task<string> GetVerificationRecordForDomainAsync(string domain)
{
var records = (await _graphClient.Domains[domain].VerificationDnsRecords.Request().GetAsync());
string verificationText = String.Empty;
foreach (var record in records)
{
if (record.RecordType == "Txt")
{
verificationText = ((Microsoft.Graph.DomainDnsTxtRecord)record).Text;
break;
}
}
return verificationText;
}
public async Task VerifyDomainAsync(string domain)
{
await _graphClient.Domains[domain].Verify().Request().PostAsync();
}
}
_graphClient should be init with access_token, but I want to have lazy loading, therefore I don't want to add it to constructor.
Ok, one solution is add a property:
public string AccessToken { set => _graphClient = (new GraphSdkHelper()).GetAuthenticatedClient(value); }
It works fine, if we remember to set AccessToken before calling any method. But if we forget to do it? The best way it is to call getting access_token by another service if this _graphClient is not init. How to do it carefully?
Why not use Lazy Initialization ?
Please take a look at the docs here

C# Net Core 2.0 refactoring

Writing code for controllers could lead to repeat myself again and again.
How can reuse the code below and apply DRY principle on C# Net Core 2.0. MVC controllers?
See the below example.
The coding for getting a full list of departments using EF and web API is as follows..
[HttpGet]
public async Task<IActionResult> Department()
{
using (var client = await _apiHttpClient.GetHttpClientAsync())
{
var response = await client.GetAsync("api/Department");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var dptos = JsonConvert.DeserializeObject<Department[]>(content);
return View(dptos);
}
if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
return RedirectToAction("AccessDenied", "Authorization");
throw new Exception($"A problem happened while calling the API: {response.ReasonPhrase}");
}
}
Is indeed almost identical to get a single department..
[HttpGet]
public async Task<IActionResult> DeparmentEdit(string id)
{
ViewData["id"] = id;
using (var client = await _apiHttpClient.GetHttpClientAsync())
{
var response = await client.GetAsync($"api/Department/{id}");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var dpto = JsonConvert.DeserializeObject<Department>(content);
return View(dpto);
}
if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
return RedirectToAction("AccessDenied", "Authorization");
throw new Exception($"A problem happened while calling the API: {response.ReasonPhrase}");
}
}
The _apiHttpClient field holds a custom implementation of an HttpClient for tokens and refreshing tokens to access the web API.
I think that IS NOT relevant here to apply refactoring and DRY but anyway I will copy his implementation here below.
BR and thanks in advance for your reply.
public class ApiHttpClient : IApiHttpClient
{
private HttpClient _httpClient;
private HttpClient HttpClient => _httpClient ?? (_httpClient = new HttpClient());
private readonly IHttpContextAccessor _httpContextAccessor;
public ApiHttpClient(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async Task<HttpClient> GetHttpClientAsync()
{
string accessToken;
var context = _httpContextAccessor.HttpContext;
var expiresAt = await context.GetTokenAsync(Constants.Tokens.ExpiresAt); // Get expires_at value
if (string.IsNullOrWhiteSpace(expiresAt) // Should we renew access & refresh tokens?
|| (DateTime.Parse(expiresAt).AddSeconds(-60)).ToUniversalTime() < DateTime.UtcNow) // Make sure to use the exact UTC date formats for comparison
{
accessToken = await RefreshTokensAsync(_httpContextAccessor.HttpContext); // Get the current HttpContext to access the tokens
}
else
{
accessToken = await context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); // Get access token
}
HttpClient.BaseAddress = new Uri(Constants.Urls.ApiHost);
if (!string.IsNullOrWhiteSpace(accessToken))
HttpClient.SetBearerToken(accessToken);
return HttpClient;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected void Dispose(bool disposing)
{
if (disposing)
{
if (_httpClient != null)
{
_httpClient.Dispose();
_httpClient = null;
}
}
}
public static async Task<string> RefreshTokensAsync(HttpContext context)
{
var discoveryResponse = await DiscoveryClient.GetAsync(Constants.Authority); // Retrive metadata information about our IDP
var tokenClient = new TokenClient(discoveryResponse.TokenEndpoint, Constants.ClientMvc.Id, Constants.ClientMvc.Secret); // Get token client using the token end point. We will use this client to request new tokens later on
var refreshToken = await context.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken); // Get the current refresh token
var tokenResponse = await tokenClient.RequestRefreshTokenAsync(refreshToken); // We request a new pair of access and refresh tokens using the current refresh token
if (tokenResponse.IsError)
return null; // Let's the unauthorized page bubbles up
// throw new Exception("Problem encountered while refreshing tokens", tokenResponse.Exception);
var expiresAt = (DateTime.UtcNow
+ TimeSpan.FromSeconds(tokenResponse.ExpiresIn)).ToString("O", CultureInfo.InvariantCulture); // New expires_at token ISO 860
var authenticateResult = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); // HttpContext.Authentication.GetAuthenticateInfoAsync() deprecated
authenticateResult.Properties.UpdateTokenValue(OpenIdConnectParameterNames.AccessToken, tokenResponse.AccessToken); // New access_token
authenticateResult.Properties.UpdateTokenValue(OpenIdConnectParameterNames.RefreshToken, tokenResponse.RefreshToken); // New refresh_token
authenticateResult.Properties.UpdateTokenValue(Constants.Tokens.ExpiresAt, expiresAt); // New expires_at token ISO 8601 WHY _at TODO
await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, authenticateResult.Principal, authenticateResult.Properties); // Signing in again with the new values, doing such a user relogin, ensuring that we change the cookies on client side. Doig so the user that has logged in has the refreshed tokens
return tokenResponse.AccessToken;
}
public static async Task RevokeTokensAsync(HttpContext context)
{
var discoveryResponse = await DiscoveryClient.GetAsync(Constants.Authority); // Retrive metadata information about our IDP
var revocationClient = new TokenRevocationClient(discoveryResponse.RevocationEndpoint, Constants.ClientMvc.Id, Constants.ClientMvc.Secret); // Get token revocation client using the token revocation endpoint. We will use this client to revoke tokens later on
var accessToken = await context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); // Get the access token token to revoke
if (!string.IsNullOrWhiteSpace(accessToken))
{
var revokeAccessTokenTokenResponse = await revocationClient.RevokeAccessTokenAsync(accessToken);
if (revokeAccessTokenTokenResponse.IsError)
throw new Exception("Problem encountered while revoking the access token.", revokeAccessTokenTokenResponse.Exception);
}
var refreshToken = await context.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken); // Get the refresh token to revoke
if (!string.IsNullOrWhiteSpace(refreshToken))
{
var revokeRefreshTokenResponse = await revocationClient.RevokeRefreshTokenAsync(refreshToken);
if (revokeRefreshTokenResponse.IsError)
throw new Exception("Problem encountered while revoking the refresh token.", revokeRefreshTokenResponse.Exception);
}
}
}
I had refactored the code as follows having in mind the following workflow.
We will need: a) an API service class, b) a HttpContextAccessor and c) a HttpClient.
1) DI principle!. We register them in our dependency injection container at ConfigureServices
services
.AddTransient<IGameApiService, GameApiService>()
.AddSingleton<IHttpContextAccessor, HttpContextAccessor>()
.AddSingleton(c => new HttpClient { BaseAddress = new Uri(Constants.Urls.ApiHost) });
2) The big job!. The new GameApiService will do the "heavy job" of calling our API methods. We will call the API using a "composed" request string. The API service will use our HttpClient, passing our request string and returning the response code and A STRING! (instead of using generics or other object) with the content. (I would need help on moving to generic since I fear that the registration on the dependency container will be "hard" to do with generics).
(the HttpContextAccessor is used for some token methods)
public class GameApiService : IGameApiService
{
private readonly HttpClient _httpClient;
private readonly HttpContext _httpContext;
public GameApiService(HttpClient httpClient, IHttpContextAccessor httpContextAccessor)
{
_httpClient = httpClient;
_httpContext = httpContextAccessor.HttpContext;
_httpClient.AddBearerToken(_httpContext); // Add current access token to the authorization header
}
public async Task<(HttpResponseMessage response, string content)> GetDepartments()
{
return await GetAsync(Constants.EndPoints.GameApi.Department); // "api/Department"
}
public async Task<(HttpResponseMessage response, string content)> GetDepartmenById(string id)
{
return await GetAsync($"{Constants.EndPoints.GameApi.Department}/{id}"); // "api/Department/id"
}
private async Task<(HttpResponseMessage response, string content)> GetAsync(string request)
{
string content = null;
var expiresAt = await _httpContext.GetTokenAsync(Constants.Tokens.ExpiresAt); // Get expires_at value
if (string.IsNullOrWhiteSpace(expiresAt) // Should we renew access & refresh tokens?
|| (DateTime.Parse(expiresAt).AddSeconds(-60)).ToUniversalTime() < DateTime.UtcNow) // Make sure to use the exact UTC date formats for comparison
{
var accessToken = await _httpClient.RefreshTokensAsync(_httpContext); // Try to ge a new access token
if (!string.IsNullOrWhiteSpace(accessToken)) // If succeded set add the new access token to the authorization header
_httpClient.AddBearerToken(_httpContext);
}
var response = await _httpClient.GetAsync(request);
if (response.IsSuccessStatusCode)
{
content = await response.Content.ReadAsStringAsync();
}
else if (response.StatusCode != HttpStatusCode.Unauthorized && response.StatusCode != HttpStatusCode.Forbidden)
{
throw new Exception($"A problem happened while calling the API: {response.ReasonPhrase}");
}
return (response, content);
}
}
public interface IGameApiService
{
Task<(HttpResponseMessage response, string content)> GetDepartments();
Task<(HttpResponseMessage response, string content)> GetDepartmenById(string id);
}
3) Great DRY! Our MVC controller will use this new API service as follows.. (we really don't have very much code there and THIS IS THE GOAL.. ;-) GREAT!!.
We still keep the responsibility of de-serialize the content string on the controller action on which the service API method was invoked. The code for the service API looks like...
[Route("[controller]/[action]")]
public class DepartmentController : Controller
{
private readonly IGameApiService _apiService;
public DepartmentController(IGameApiService apiService)
{
_apiService = apiService;
}
[HttpGet]
public async Task<IActionResult> Department()
{
ViewData["Name"] = User.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Name)?.Value;
var (response, content) = await _apiService.GetDepartments();
if (!response.IsSuccessStatusCode) return Forbid();
return View(JsonConvert.DeserializeObject<Department[]>(content));
}
[HttpGet]
public async Task<IActionResult> DepartmentEdit(string id)
{
ViewData["id"] = id;
var (response, content) = await _apiService.GetDepartmenById(id);
if (!response.IsSuccessStatusCode) return Forbid();
return View(JsonConvert.DeserializeObject<Department>(content));
}
}
4) Last trick!. To redirect to a custom page when we are not authorized or the permission has been denied we have issued if (!response.IsSuccessStatusCode) return Forbid(); yes Forbid(). But we still need to configure the default denied page on the cookie middleware. Thus on ConfigureServices we do it with services.AddAuthentication().AddCookie(AddCookie) methods, configuring the relevant options, mainly the AccessDeniedPath option as follows.
private static void AddCookie(CookieAuthenticationOptions options)
{
options.Cookie.Name = "mgame";
options.AccessDeniedPath = "/Authorization/AccessDenied"; // Redirect to custom access denied page when user get access is denied
options.Cookie.HttpOnly = true; // Prevent cookies from being accessed by malicius javascript code
options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // Cookie only will be sent over https
options.ExpireTimeSpan = TimeSpan.FromMinutes(Constants.CookieTokenExpireTimeSpan); // Cookie will expire automaticaly after being created and the client will redirect back to Identity Server
}
5) A word about the HTTP Client!. It will be instantiated using a factory on the dependency injection. A new instance is created per GameApiService instance.
The helper code to set the bearer token on the header and refresh the access token has been moved to a convenient extension method helper class as follows..
public static class HttpClientExtensions
{
public static async void AddBearerToken(this HttpClient client, HttpContext context)
{
var accessToken = await context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
if (!string.IsNullOrWhiteSpace(accessToken))
client.SetBearerToken(accessToken);
}
public static async Task<string> RefreshTokensAsync(this HttpClient client, HttpContext context)
{
var discoveryResponse = await DiscoveryClient.GetAsync(Constants.Authority); // Retrive metadata information about our IDP
var tokenClient = new TokenClient(discoveryResponse.TokenEndpoint, Constants.ClientMvc.Id, Constants.ClientMvc.Secret); // Get token client using the token end point. We will use this client to request new tokens later on
var refreshToken = await context.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken); // Get the current refresh token
var tokenResponse = await tokenClient.RequestRefreshTokenAsync(refreshToken); // We request a new pair of access and refresh tokens using the current refresh token
if (tokenResponse.IsError) // Let's the unauthorized page bubbles up instead doing throw new Exception("Problem encountered while refreshing tokens", tokenResponse.Exception)
return null;
var expiresAt = (DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn)).ToString("O", CultureInfo.InvariantCulture); // New expires_at token ISO 860
var authenticateResult = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); // HttpContext.Authentication.GetAuthenticateInfoAsync() deprecated
authenticateResult.Properties.UpdateTokenValue(OpenIdConnectParameterNames.AccessToken, tokenResponse.AccessToken); // New access_token
authenticateResult.Properties.UpdateTokenValue(OpenIdConnectParameterNames.RefreshToken, tokenResponse.RefreshToken); // New refresh_token
authenticateResult.Properties.UpdateTokenValue(Constants.Tokens.ExpiresAt, expiresAt); // New expires_at token ISO 8601
await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, authenticateResult.Principal, authenticateResult.Properties); // Signing in again with the new values, doing such a user relogin, ensuring that we change the cookies on client side. Doig so the user that has logged in has the refreshed tokens
return tokenResponse.AccessToken;
}
public static async Task RevokeTokensAsync(this HttpClient client, HttpContext context)
{
var discoveryResponse = await DiscoveryClient.GetAsync(Constants.Authority); // Retrive metadata information about our IDP
var revocationClient = new TokenRevocationClient(discoveryResponse.RevocationEndpoint, Constants.ClientMvc.Id, Constants.ClientMvc.Secret); // Get token revocation client using the token revocation endpoint. We will use this client to revoke tokens later on
var accessToken = await context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); // Get the access token token to revoke
if (!string.IsNullOrWhiteSpace(accessToken))
{
var revokeAccessTokenTokenResponse = await revocationClient.RevokeAccessTokenAsync(accessToken);
if (revokeAccessTokenTokenResponse.IsError)
throw new Exception("Problem encountered while revoking the access token.", revokeAccessTokenTokenResponse.Exception);
}
var refreshToken = await context.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken); // Get the refresh token to revoke
if (!string.IsNullOrWhiteSpace(refreshToken))
{
var revokeRefreshTokenResponse = await revocationClient.RevokeRefreshTokenAsync(refreshToken);
if (revokeRefreshTokenResponse.IsError)
throw new Exception("Problem encountered while revoking the refresh token.", revokeRefreshTokenResponse.Exception);
}
}
}
Now the code after refactoring it looks more pretty and clean.. ;-)
You could just split it up using generics. I haven't debugged this code (obviously), but I think it gets you where you need to go.
using System.Security.Authentication;
[HttpGet]
public async Task<IActionResult> Department() {
try {
var myObject = await GetSafeData<Department[]>("api/Department");
return view(myObj);
} catch(AuthenticationException ex) {
return RedirectToAction("AccessDenied", "Authorization");
}
}
internal T GetSafeData<T>(string url) {
using (var client = await _apiHttpClient.GetHttpClientAsync()) {
var response = await client.GetAsync(url);
if (response.IsSuccessStatusCode) {
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(content);
}
if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
Throw New AuthenticationException("");
throw new Exception($"A problem happened while calling the API: {response.ReasonPhrase}");
}
}
You can sorta see how you might pass response to that same method, so you could do your AccessDenied redirect within that method as well and reduce your repetitive code everywhere.
It's a generic method, so you can use it for ANY call to that api. That should be enough to get you started. Hope it helps!

Categories

Resources