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
Related
I have a .net core 3.1 web API ,which was built with JWT authentication and it is integrated with Angular UI and it working as expected.
following is my JWT authentication middleware
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
// Adding Jwt Bearer
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.IncludeErrorDetails = true;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = false,
ValidateAudience = false,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"]))
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
Now i need to create some more Web API methods which will be consumed by Angular UI as well as some existing scheduled tasks (.net console applications which will be consuming the web api methods) which are created for internal operations and will be running in the background.
My API controllers are decorated with [Authorize] attribute. It is working fine with Angular UI where the authentication and authorization are implemented using JWT bearer token.The problem is now with the integration of scheduled tasks which does not have logic for getting the tokens.
How to integrate these console apps with .net core web API in terms of authentication? the easiest option (which i thought) is to create a user login with like username "servicetask" and obtain token based on that username and do the API operation (but this requires more effort since no.of console apps are more and there and some apps from other projects also).
Is there any way to handle authentication in this case?
Is it good practice to pass some API key from console application and by pass the authentication in web API ? is that possible ? then how to handle the request in .net core web api?
Is it possible to create any JWT role or claims for these service account and validate them?
Please help.
Best approach would be to allow both bearer token and API key authorization, especially since you are allowing access for users and (internal) services.
Add API key middleware (I personally use this, it's simple to use - package name is AspNetCore.Authentication.ApiKey) with custom validation (store API keys in database along with regular user data or in config, whatever you prefer). Modify [Authorize] attributes on controllers so both Bearer and ApiKey authorization can be used. Angular app continues to use Bearer authentication and any service/console apps (or any other client, including Angular client if needed in some case) sends X-Api-Key header containing API key assigned to that app.
Middleware configuration should look something like this:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddApiKeyInHeader(ApiKeyDefaults.AuthenticationScheme, options =>
{
options.KeyName = "X-API-Key";
options.SuppressWWWAuthenticateHeader = true;
options.Events = new ApiKeyEvents
{
// A delegate assigned to this property will be invoked just before validating the api key.
OnValidateKey = async (context) =>
{
var apiKey = context.ApiKey.ToLower();
// custom code to handle the api key, create principal and call Success method on context. apiUserService should look up the API key and determine is it valid and which user/service is using it
var apiUser = apiUserService.Validate(apiKey);
if (apiUser != null)
{
... fill out the claims just as you would for user which authenticated using Bearer token...
var claims = GenerateClaims();
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
else
{
// supplied API key is invalid, this authentication cannot proceed
context.NoResult();
}
}
};
})
// continue with JwtBearer code you have
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, x => ...
This sorts out Startup.cs part.
Now, in controllers, where you want to enable both Bearer and ApiKey authentication, modify attribute so it looks like this:
[Route("api/[controller]")]
[ApiController]
[Authorize(AuthenticationSchemes = "ApiKey, Bearer")]
public class SomeController : ControllerBase
Now, Angular client will still work in the same way but console app might call API like this:
using (HttpClient client = new HttpClient())
{
// header must match definition in middleware
client.DefaultRequestHeaders.Add("X-API-Key", "someapikey");
client.BaseAddress = new Uri(url);
using (HttpResponseMessage response = await client.PostAsync(url, q))
{
using (HttpContent content =response.Content)
{
string mycontent = await content.ReadAsStringAsync();
}
}
}
This approach in my opinion makes best use of AuthenticationHandler and offers cleanest approach of handling both "regular" clients using JWT and services using fixed API keys, closely following something like OAuth middleware. More details about building custom authentication handler if someone wants to build something like this from scratch, implementing basically any kind of authentication.
Downside of course is security of those API keys even if you are using them for internal services only. This problem can be remedied a bit by limiting access scope for those API keys using Claims, not using same API key for multiple services and changing them periodically. Also, API keys are vulnerable to interception (MITM) if SSL is not used so take care of that.
1.IMO, no , this won't be good idea.
2. Yes you can use claims for this scenario .
Use a BackgroundService to run your task and inject claims principle on this class.
This sample is for service provider account claims:
serviceAccountPrincipleProvider.cs
public class ServiceAccountPrincipalProvider : IClaimsPrincipalProvider
{
private readonly ITokenProvider tokenProvider;
public ServiceAccountPrincipalProvider(ITokenProvider tokenProvider)
{
this.tokenProvider = tokenProvider;
}
public ClaimsPrincipal CurrentPrincipal
{
get
{
var accessToken = tokenProvider.GetAccessTokenAsync().GetAwaiter().GetResult();
if (accessToken == null)
return null;
var identity = new ClaimsIdentity(AuthenticationTypes.Federation);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accessToken.Subject));
identity.AddClaim(new Claim(AppClaimTypes.Issuer, accessToken.Issuer));
identity.AddClaim(new Claim(AppClaimTypes.AccessToken, accessToken.RawData));
return new ClaimsPrincipal(identity);
}
}
}
This is your IClaimsProvider interface:
public interface IClaimsPrincipalProvider
{
ClaimsPrincipal CurrentPrincipal { get; }
}
Don`t bypass authentication. You can pass appKey (key to identify the app instance) to webapi endpoint that is responsible for identifying your dotnet console apps. If appkey is part of your registered appkeys list, let the webapi endpoint get token on behalf of the console app by subsequent authentication step with your webapi auth service and return a JWT token to the console app.
In my case I have console apps running on dotnet 4.5, I mention this because HttpClient is not available in previous versions. With HttpClient you can then do the following in your console app.
HttpClient client = new HttpClient();
client.BaseAddress = new Uri("localhost://mywebapi/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/text"));
HttpResponseMessage response= client.GetAsync("api/appidentityendpoint/appkey").GetAwaiter().GetResult();
var bytarr = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
string responsemessage = Encoding.UTF8.GetString(bytarr);
res = JsonConvert.DeserializeObject<Authtoken>(responsemessage);
Authtoken object can be as simple as
public class Authtoken
{
public string JwtToken{ get; set; }
}
and once you have your token you add it to your HttpClient headers for subsequent calls to your protected endpoints
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + res.JwtToken);
client.GetAsync("api/protectedendpoint").GetAwaiter().GetResult();
Error handling is obviously required to handle reauthentication in case of token expiry etc
On server side, a simplified example is as follows
[Produces("application/json")]
[Route("api/Auth")]
public class AuthController : Controller
{
private readonly IAppRegService _regAppService;
public AuthController(IAppRegService regAppService){
_regAppService = regAppService;
};
//api/auth/console/login/585
[HttpGet, Route("console/login/{appkey}")]
public async Task<IActionResult> Login(string appkey)
{
// write logic to check in your db if appkey is the key of a registered console app.
// _regAppService has methods to connect to db or read file to check if key exists from your repository of choice
var appkeyexists = _regAppService.CheckAppByAppKey(appkey);
if(appkeyexists){
//create claims list
List<Claim> claims = new List<Claim>();
claims.Add(new Claim("appname", "console",ClaimValueTypes.String));
claims.Add(new Claim("role","daemon",ClaimValueTypes.String));
//create a signing secret
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("yoursecretkey"));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
//create token options
var tokenOptions = new JwtSecurityToken(
issuer: "serverurl",
audience:"consoleappname",
claims: claims,
expires: DateTime.Now.AddDays(5),
signingCredentials: signinCredentials
);
//create token
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions);
//return token
return new OkObjectResult(new Authtoken { JwtToken= tokenString });
} else {
return Unauthorized();
}
}
}
I will present how I do JWT Auth from a WebAssembly app to a .NET Core API. Everything is based on this YouTube video. It explains everything you need to know. Down below is a sample of code from the video to give you an idea of what you have to do.
This is my Auth Controller:
// A bunch of usings
namespace Server.Controllers.Authentication
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class AuthenticateController : ControllerBase
{
private readonly UserManager<ApplicationUser> userManager;
private readonly RoleManager<IdentityRole> roleManager;
private readonly IConfiguration _configuration;
private readonly AppContext appContext;
public AuthenticateController(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration configuration, AppContext appContext)
{
this.userManager = userManager;
this.roleManager = roleManager;
this._configuration = configuration;
this.appContext = appContext;
}
[HttpPost]
[Route("login")]
[AllowAnonymous]
public async Task<IActionResult> Login([FromBody] LoginModel loginModel)
{
ApplicationUser user = await userManager.FindByNameAsync(loginModel.Username);
if ((user is not null) && await userManager.CheckPasswordAsync(user, loginModel.Password))
{
IList<string> userRoles = await userManager.GetRolesAsync(user);
List<Claim> authClaims = new()
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.AuthenticationMethod, "pwd")
};
foreach (string role in userRoles)
{
authClaims.Add(new Claim(ClaimTypes.Role, role));
}
SymmetricSecurityKey authSigningKey = new(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));
//SymmetricSecurityKey authSigningKey = Startup.SecurityAppKey;
JwtSecurityToken token = new(
issuer: _configuration["JWT:ValidIssuer"],
//audience: _configuration["JWT:ValidAudience"],
expires: DateTime.Now.AddHours(3),
claims: authClaims,
signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
return Unauthorized();
}
[HttpPost]
[Route("register")]
[AllowAnonymous]
public async Task<IActionResult> Register([FromBody] RegisterModel model)
{
ApplicationUser userExists = await userManager.FindByNameAsync(model.Username);
if (userExists != null)
{
return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User already exists!" });
}
ApplicationUser user = new()
{
Email = model.Email,
SecurityStamp = Guid.NewGuid().ToString(),
UserName = model.Username
};
IdentityResult result = await userManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User creation failed! Please check user details and try again." });
}
await userManager.AddToRoleAsync(user, UserRoles.User);
return Ok(new Response { Status = "Success", Message = "User created successfully!" });
}
}
}
When a user registers it is automatically added to the User Role. What you should do is to create accounts for each of your console apps, or even a global account for all internal apps, and then assign it to a custom role.
After that, on all API endpoints that are only accessible by your internal apps add this attribute: [Authorize(Roles = UserRoles.Internal)]
UserRoles is a static class that has string properties for each role.
More info about Role-based authorization can be found here.
You can create a login password configuration on appsettings, db or somewhere to send a token (web api).
Worker.cs (console app)
public struct UserLogin
{
public string user;
public string password;
}
// ...
private async Task<string> GetToken(UserLogin login)
{
try {
string token;
var content = new StringContent(JsonConvert.SerializeObject(login), Encoding.UTF8, "application/json");
using (var httpClient = new HttpClient())
using (var response = await httpClient.PostAsync($"{api}/login", content))
{
var result = await response.Content.ReadAsStringAsync();
var request = JsonConvert.DeserializeObject<JObject>(result);
token = request["token"].ToObject<string>();
}
return token;
}
catch (Exception e)
{
throw new Exception(e.Message);
}
}
Give your console a jwt token without an expiration date or one that gives you enough time. If you need to invalidate the token follow this link. Add the jwt on appsettings.json and read the token as follows:
appsettings.json
{
//...
"Worker" : "dotnet",
"Token": "eyJhbGciOiJIUzI1Ni...",
"ApiUrl": "http://localhost:3005",
//...
}
Worker.cs
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
public Worker(ILogger<Worker> _logger, IConfiguration _cfg)
{
logger = _logger;
//...
api = _cfg["ApiUrl"];
token = _cfg["Token"];
}
private async Task SendResult(SomeModel JobResult)
{
var content = new StringContent(JsonConvert.SerializeObject(JobResult), Encoding.UTF8, "application/json");
using (var httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
using (var response = await httpClient.PostAsync($"{api}/someController", content))
{
var result = await response.Content.ReadAsStringAsync();
var rs = JsonConvert.DeserializeObject(result);
Console.WriteLine($"API response {response.ReasonPhrase}");
}
}
}
Update:
If you need to control requests:
Startup.cs
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters()
{
// ...
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = TokenValidation
};
});
private static Task TokenValidation(TokenValidatedContext context)
{
// your custom validation
var hash = someHashOfContext();
if (context.Principal.FindFirst(ClaimTypes.Hash).Value != hash)
{
context.Fail("You cannot access here");
}
return Task.CompletedTask;
}
I have custom AuthenticationStateProvider in my Blazor WebAssembly app and it's not calling GetAuthenticationStateAsync method on page refresh so if user is logged in and then they manually refresh page ClaimsPrincipal is AuthenticationState is not populated so user can't be Authorized.
I'm using JWT which is saved in cookie for auth, and that cookie remains after page refresh, app just doesn't call GetAuthenticationStateAsync (which it should on page refresh if I learned blazor correctly).
Here's my custom AuthenticationStateProvider:
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly HttpClient _httpClient;
public CustomAuthenticationStateProvider(
IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient("APIClient");
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
//TODO get token from cookie
var savedToken = "";
if (string.IsNullOrWhiteSpace(savedToken))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(savedToken), "jwt")));
}
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);
}
}
EDIT: It was calling GetAuthenticationStateAsync method the entire time, it was just confusing because I was in debugger and it never hit my breakpoint inside of that method, which is problem I saw other people (still not sure why is that).
It depends on where you call GetAuthenticationStateAsync.
i.e. in BlazorHero project this call is present in the MainLayout.razor:
</NotAuthorized>
<Authorized>
#(LoadDataAsync())
<MudLayout RightToLeft="#_rightToLeft">
in MainLayout.razor.cs:
private async Task LoadDataAsync()
{
var state = await _stateProvider.GetAuthenticationStateAsync();
var user = state.User;
if (user == null) return;
so I think you need to check where it is called.
The MainLayout is rendered every time you load a page.
A better solution (cleaner) is to add this code in the AfterRender like:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await LoadDataAsync();
}
}
Summary
I have ASP.NET MVC 5 web app with Identity authentication and I have to develop an API with "grant_type" = "authorization_code". This API will be to provide users data to another "well-known" web service that needs a custom error responses. My IDE is Visual Studio Professional 2017. I use Postman to make requests to my Web API.
Documentation I read
In the OWIN and Katana documentation the OWIN OAuth 2.0 Authorization Server link redirects again to main OWIN and Katana page, but I think that I found the source on GitHub: OWIN OAuth 2.0 Authorization Server. I tried to follow this documentation, but there are no examples about this question.
Problem
I can create a new authorization code in my AuthorizationCodeProvider class (with Create() method) when a user authenticates and authorizes the "well-known" web service client to access user's resources. I store this code in a database. When I request a Token AuthorizationCodeProvider.Receive() method is called and the token is deserialized correctly. Then GrantAuthorizationCode() method is called, Postman receives OK response (200 status code) but without token information in body (.AspNet.ApplicationCookie is in cookies).
Detailed explanation and code
This is the Startup class:
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)),
OnApplyRedirect = (context =>
{
// This code is to return custom error response
string path = null;
if (context.Request.Path.HasValue)
path = context.Request.Path.Value;
if (!(path != null && path.Contains("/api"))) // Don't redirect to login page
context.Response.Redirect(context.RedirectUri);
})
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
this.ConfigureAuthorization(app);
}
private void ConfigureAuthorization(IAppBuilder app)
{
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
OAuthOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = false,
TokenEndpointPath = new PathString("/api/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new TokenAuthorizationServerProvider(),
AuthorizationCodeProvider = new AuthorizationCodeProvider()
};
app.Use<AuthenticationMiddleware>(); //Customize responses in Token middleware
app.UseOAuthAuthorizationServer(OAuthOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
ConfigureAuthorization() method configures the authorization. It uses classes implemented by me:
AuthenticationMiddleware: the well-known web service wants 401 status responses with custom error JONS instead of the usual 400 status response. It is based on the answer of the question Replace response body using owin middleware.
public class AuthenticationMiddleware : OwinMiddleware
{
public AuthenticationMiddleware(OwinMiddleware next) : base(next) { }
public override async Task Invoke(IOwinContext context)
{
var owinResponse = context.Response;
var owinResponseStream = owinResponse.Body;
var responseBuffer = new MemoryStream();
owinResponse.Body = responseBuffer;
await Next.Invoke(context);
if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest &&
context.Response.Headers.ContainsKey(BearerConstants.CustomUnauthorizedHeaderKey))
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
string headerValue = context.Response.Headers.Get(BearerConstants.CustomUnauthorizedHeaderKey);
context.Response.Headers.Remove(BearerConstants.CustomUnauthorizedHeaderKey);
ErrorMessage errorMessage = new ErrorMessage(headerValue);
string json = JsonConvert.SerializeObject(errorMessage, Formatting.Indented);
var customResponseBody = new StringContent(json);
var customResponseStream = await customResponseBody.ReadAsStreamAsync();
await customResponseStream.CopyToAsync(owinResponseStream);
owinResponse.ContentType = "application/json";
owinResponse.ContentLength = customResponseStream.Length;
owinResponse.Body = owinResponseStream;
}
}
}
When ErrorMessage is serialized to JSON returns an array of errors:
{
"errors":
[
"message": "the error message"
]
}
I set the BearerConstants.CustomUnauthorizedHeaderKey header in TokenAuthorizationServerProvider.ValidateClientAuthentication() method using a extension method:
public static void Rejected(this OAuthValidateClientAuthenticationContext context, string message)
{
Debug.WriteLine($"\t\t{message}");
context.SetError(message);
context.Response.Headers.Add(BearerConstants.CustomUnauthorizedHeaderKey, new string[] { message });
context.Rejected();
}
This is how TokenAuthorizationServerProvider is implemented:
public class TokenAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
{
// Only for breakpoint. Never stops.
return base.AuthorizeEndpoint(context);
}
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Check if grant_type is authorization_code
string grantType = context.Parameters[BearerConstants.GrantTypeKey];
if (string.IsNullOrEmpty(grantType) || grantType != BearerConstants.GrantTypeAuthorizationCode)
{
context.Rejected("Invalid grant type"); // Sets header for custom response
return;
}
// Check if client_id and client_secret are in the request
string clientId = context.Parameters[BearerConstants.ClientIdKey];
string clientSecret = context.Parameters[BearerConstants.ClientSecretKey];
if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret))
{
context.Rejected("Client credentials missing"); // Sets header for custom response
return;
}
//Check if client_id and client_secret are valid
ApiClient client = await (new ApiClientService()).ValidateClient(clientId, clientSecret);
if (client != null)
{
// Client has been verified.
Debug.WriteLine($"\t\tClient has been verified");
context.OwinContext.Set<ApiClient>("oauth:client", client);
context.Validated(clientId);
}
else
{
// Client could not be validated.
context.Rejected("Invalid client"); // Sets header for custom response
}
}
public override async Task GrantAuthorizationCode(OAuthGrantAuthorizationCodeContext context)
{
TokenRequestParameters parameters = await context.Request.GetBodyParameters();
using (IUserService userService = new UserService())
{
ApplicationUser user = await userService.ValidateUser(parameters.Code);
if (user == null)
{
context.Rejected("Invalid code");
return;
}
// Initialization.
var claims = new List<Claim>();
// Setting
claims.Add(new Claim(ClaimTypes.Name, user.UserName));
// Setting Claim Identities for OAUTH 2 protocol.
ClaimsIdentity oAuthClaimIdentity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesClaimIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationType);
// Setting user authentication.
IDictionary<string, string> data = new Dictionary<string, string>{ { "userName", user.UserName } };
AuthenticationProperties properties = new AuthenticationProperties(data);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthClaimIdentity, properties);
// Grant access to authorize user.
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesClaimIdentity);
}
}
}
ApiClientService.ValidateClient() checks on database that cliend ID and Secret are correct.
GrantAuthorizationCode() is based on the step 8 from ASP.NET MVC - OAuth 2.0 REST Web API Authorization Using Database First Approach tutorial. But this tutorial for grant_type = password and I think that something is wrong in here.
And the AuthorizationCodeProvider class:
public class AuthorizationCodeProvider : AuthenticationTokenProvider
{
public override void Create(AuthenticationTokenCreateContext context)
{
AuthenticationTicket ticket = context.Ticket;
string serializedTicket = context.SerializeTicket();
context.SetToken(serializedTicket);
}
public override void Receive(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
// At this point context.Ticket.Identity.IsAuthenticated is true
}
}
I call to create method from the AuthorizationController that shows the Allow/Deny view. It is decorated with System.Web.Mvc.Authorize attribute, so if the user isn't authenticated he or she has to login using the default login page from MVC template project (/account/login):
[Authorize]
public class AuthorizationController : Controller
{
private const string ServiceScope = "service-name";
[HttpGet]
public async Task<ActionResult> Index(string client_id, string response_type, string redirect_uri, string scope, string state)
{
AuthorizationViewModel vm = new AuthorizationViewModel()
{
ClientId = client_id,
RedirectUri = redirect_uri,
Scope = scope,
State = state
};
if (scope == ServiceScope)
{
var authentication = HttpContext.GetOwinContext().Authentication;
authentication.SignIn(
new AuthenticationProperties { IsPersistent = true, RedirectUri = redirect_uri },
new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, User.Identity.Name) },
"Bearer"));
}
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
[MultiButton(MatchFormKey = "authorization", MatchFormValue = "Allow")]
public async Task<ActionResult> Allow(AuthorizationViewModel vm)
{
if (ModelState.IsValid)
{
string code = await this.SetAuthorizationCode(vm.ClientId, vm.RedirectUri);
if (vm.Scope == ServiceScope)
{
string url = $"{vm.RedirectUri}?code={code}&state={vm.State}";
return Redirect(url);
}
else
{
return Redirect(vm.RedirectUri);
}
}
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
[MultiButton(MatchFormKey = "authorization", MatchFormValue = "Deny")]
public async Task<ActionResult> Deny(AuthorizationViewModel vm)
{
// Removed for brevity
return View(vm);
}
private async Task<string> SetAuthorizationCode(string clientId, string redirectUri)
{
string userId = User.Identity.GetUserId();
ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(clientId, OAuthDefaults.AuthenticationType));
AuthenticationTokenCreateContext authorizeCodeContext = new AuthenticationTokenCreateContext(
HttpContext.GetOwinContext(),
Startup.OAuthOptions.AuthorizationCodeFormat,
new AuthenticationTicket(
identity,
new AuthenticationProperties(new Dictionary<string, string>
{
{ "user_id", userId },
{ "client_id", clientId },
{ "redirect_uri", redirectUri }
})
{
IssuedUtc = DateTimeOffset.UtcNow,
ExpiresUtc = DateTimeOffset.UtcNow.Add(Startup.OAuthOptions.AuthorizationCodeExpireTimeSpan)
}));
Startup.OAuthOptions.AuthorizationCodeProvider.Create(authorizeCodeContext);
string code = authorizeCodeContext.Token;
IUserService userService = new UserService();
await userService.SetAuthorization(userId, true, code); // save to database
userService.Dispose();
return code;
}
}
The authorization code is created in SetAuthorizationCode() method, which is called in Allow() action. This SetAuthorizationCode() method code is based on this answer.
Questions
I now that is very long with a lot of code, but I'm stuck for some days and I didn't find the solution. I don't know the complete flow of the authorization, I think that I'm missing something.
What happens when I call /api/token? I mean, what are the steps in this part of the authentication/authorization flow?
What happens after AuthorizationCodeProvider.GrantAuthorizationCode()?
Why a cookie returned instead of token in the body?
I found the solution of the problem, it was the AuthenticationMiddleware. Once the body of the response is read, it remains empty and does not reach the client. So you have to rewrite the response body.
public class AuthenticationMiddleware : OwinMiddleware
{
public AuthenticationMiddleware(OwinMiddleware next) : base(next) { }
public override async Task Invoke(IOwinContext context)
{
var owinResponse = context.Response;
var owinResponseStream = owinResponse.Body;
var responseBuffer = new MemoryStream();
owinResponse.Body = responseBuffer;
await Next.Invoke(context);
if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest &&
context.Response.Headers.ContainsKey(BearerConstants.CustomUnauthorizedHeaderKey))
{
// Customize the response
}
else
{
// Set body again with the same content
string body = Encoding.UTF8.GetString(responseBuffer.ToArray());
StringContent customResponseBody = new StringContent(body);
Stream customResponseStream = await customResponseBody.ReadAsStreamAsync();
await customResponseStream.CopyToAsync(owinResponseStream);
}
}
}
I followed this tutorial: https://medium.com/#st.mas29/microsoft-blazor-web-api-with-jwt-authentication-part-1-f33a44abab9d
I downloaded the example: https://github.com/StuwiiDev/DotnetCoreJwtAuthentication/tree/Part2
I can see that the token is created but I don't understand how it is or should be saved on the client side as each time I access the SampleDataController, which has the Authorize tag, it returns a 401.
When calling and adding the token using Postman it works.
What am I missing for my user to be authenticated? Doesn't Microsoft.AspNetCore.Authentication.JwtBearer handle the client part (storing the token)?
What am I missing for my user to be authenticated? Doesn't Microsoft.AspNetCore.Authentication.JwtBearer handle the client part (storing the token)?
The JwtBearer runs on server side , it will only validate the authorization header of request, namely Authorization: Bearer your_access_token, and won't care about how you WebAssembly codes runs . So you need send the request with a jwt accessToken . Since the tutorial suggests you should use localStorage , let's store the accessToken with localStorage .
Because WebAssembly has no access to BOM yet, we need some javascript codes served as glue . To do that, add a helper.js under the JwtAuthentication.Client/wwwroot/js/ :
var wasmHelper = {};
wasmHelper.ACCESS_TOKEN_KEY ="__access_token__";
wasmHelper.saveAccessToken = function (tokenStr) {
localStorage.setItem(wasmHelper.ACCESS_TOKEN_KEY,tokenStr);
};
wasmHelper.getAccessToken = function () {
return localStorage.getItem(wasmHelper.ACCESS_TOKEN_KEY);
};
And reference the script in your JwtAuthentication.Client/wwwroot/index.html
<body>
<app>Loading...</app>
<script src="js/helper.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
</body>
Now, let's wrap the javascript codes into C# . Create a new file Client/Services/TokenService.cs:
public class TokenService
{
public Task SaveAccessToken(string accessToken) {
return JSRuntime.Current.InvokeAsync<object>("wasmHelper.saveAccessToken",accessToken);
}
public Task<string> GetAccessToken() {
return JSRuntime.Current.InvokeAsync<string>("wasmHelper.getAccessToken");
}
}
Register this service by :
// file: Startup.cs
services.AddSingleton<TokenService>(myTokenService);
And now we can inject the TokenService into Login.cshtml and use it to save token :
#using JwtAuthentication.Client.Services
// ...
#page "/login"
// ...
#inject TokenService tokenService
// ...
#functions {
public string Email { get; set; } = "";
public string Password { get; set; } = "";
public string Token { get; set; } = "";
/// <summary>
/// response from server
/// </summary>
private class TokenResponse{
public string Token;
}
private async Task SubmitForm()
{
var vm = new TokenViewModel
{
Email = Email,
Password = Password
};
var response = await Http.PostJsonAsync<TokenResponse>("http://localhost:57778/api/Token", vm);
await tokenService.SaveAccessToken(response.Token);
}
}
Let's say you want to send data within FetchData.cshtml
#functions {
WeatherForecast[] forecasts;
protected override async Task OnInitAsync()
{
var token = await tokenService.GetAccessToken();
Http.DefaultRequestHeaders.Add("Authorization",String.Format("Bearer {0} ",token));
forecasts = await Http.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");
}
}
and the result will be :
Apologies in advance as this is somewhat responding to a previous answer, but I don't have the rep to comment on that.
If it helps anyone else who was similarly looking for a solution to using JWT in a Blazor app, I found #itminus answer incredibly useful, but it also pointed me to another course.
One problem I found was that calling FetchData.cshtml a second time would blow up when it tries to add the Authorization header a second time.
Instead of adding the default header there, I added it to the HttpClient singleton after a successful login (which I believe Blazor creates for you automatically). So changing SubmitForm in Login.cshtml from #itminus' answer.
protected async Task SubmitForm()
{
// Remove any existing Authorization headers
Http.DefaultRequestHeaders.Remove("Authorization");
TokenViewModel vm = new TokenViewModel()
{
Email = Email,
Password = Password
};
TokenResponse response = await Http.PostJsonAsync<TokenResponse>("api/Token/Login", vm);
// Now add the token to the Http singleton
Http.DefaultRequestHeaders.Add("Authorization", string.Format("Bearer {0} ", response.Token));
}
Then I realised, than as I'm building a SPA, so I didn't need to persist the token across requests at all - it's just in attached to the HttpClient.
The following class handle the login process on the client, storing the JWT token in local storage. Note: It is the developer responsibility to store the JWT token, and passes it to the server. The client (Blazor, Angular, etc.) does not do that for him automatically.
public class SignInManager
{
// Receive 'http' instance from DI
private readonly HttpClient http;
public SignInManager(HttpClient http)
{
this.http = http;
}
[Inject]
protected LocalStorage localStorage;
public bool IsAuthenticated()
{
var token = localStorage.GetItem<string>("token");
return (token != null);
}
public string getToken()
{
return localStorage.GetItem<string>("token");
}
public void Clear()
{
localStorage.Clear();
}
// model.Email, model.Password, model.RememberMe, lockoutOnFailure: false
public async Task<bool> PasswordSignInAsync(LoginViewModel model)
{
SearchInProgress = true;
NotifyStateChanged();
var result = await http.PostJsonAsync<Object>("/api/Account", model);
if (result)// result.Succeeded
{
_logger.LogInformation("User logged in.");
// Save the JWT token in the LocalStorage
// https://github.com/BlazorExtensions/Storage
await localStorage.SetItem<Object>("token", result);
// Returns true to indicate the user has been logged in and the JWT token
// is saved on the user browser
return true;
}
}
}
// This is how you call your Web API, sending it the JWT token for // the current user
public async Task<IList<Profile>> GetProfiles()
{
SearchInProgress = true;
NotifyStateChanged();
var token = signInManager.getToken();
if (token == null) {
throw new ArgumentNullException(nameof(AppState)); //"No token";
}
this.http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// .set('Content-Type', 'application/json')
// this.http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
Profiles = await this.http.GetJsonAsync<Profile[]>("/api/Profiles");
SearchInProgress = false;
NotifyStateChanged();
}
// You also have to set the Startup class on the client as follows:
public void ConfigureServices(IServiceCollection services)
{
// Add Blazor.Extensions.Storage
// Both SessionStorage and LocalStorage are registered
// https://github.com/BlazorExtensions/Storage
**services.AddStorage();**
...
}
// Generally speaking this is what you've got to do on the client. // On the server, you've got to have a method, say in the Account controller, whose function is to generate the JWT token, you've to configure the JWT middleware, to annotate your controllers with the necessary attribute, as for instance:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
and so on...
Hope this helps...
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!