Skip to content

Commit

Permalink
Feat(#72) Add configuration for the use of refresh token. Login is al…
Browse files Browse the repository at this point in the history
…so using the body instead of the url parameters.
  • Loading branch information
iamkinetic committed Aug 13, 2018
1 parent fd5aeec commit b3ac054
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Survi.Prevention.Models.DataTransfertObjects
{
public class LoginInformations
{
public string Username { get; set; }
public string Password { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Survi.Prevention.Models.DataTransfertObjects
{
public class TokenRefreshResult
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
}
83 changes: 74 additions & 9 deletions Survi.Prevention.ServiceLayer/Services/AuthentificationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using Survi.Prevention.DataLayer;
Expand All @@ -24,19 +25,72 @@ public AuthentificationService(ManagementContext context) : base(context)
.SingleOrDefault(user => user.Username == username && user.Password == encodedPassword && user.IsActive);
if (userFound != null)
{
var token = GenerateJwtToken(userFound, applicationName, issuer, secretKey);
var handler = new JwtSecurityTokenHandler();
var tokenString = handler.WriteToken(token);
var accessToken = new AccessToken {TokenForAccess = tokenString, ExpiresIn = 600000, IdWebuser = userFound.Id};
Context.Add(accessToken);
var accessToken = GenerateAccessToken(userFound, applicationName, issuer, secretKey);
var refreshToken = GenerateRefreshToken();
var token = new AccessToken {TokenForAccess = accessToken, RefreshToken = refreshToken, ExpiresIn = (int)(TimeSpan.FromHours(9).TotalSeconds), IdWebuser = userFound.Id};
Context.Add(token);
Context.SaveChanges();
return (accessToken, userFound);
return (token, userFound);
}

return (null, null);
}

protected JwtSecurityToken GenerateJwtToken(Webuser userLoggedIn, string applicationName, string issuer, string secretKey)

public string Refresh(string token, string refreshToken, string applicationName, string issuer, string secretKey)
{
var webuserId = GetCallDispatcherIdFromExpiredToken(token, issuer, applicationName, secretKey);
var webuserToken = Context.AccessTokens.Include(t => t.User)
.FirstOrDefault(t => t.IdWebuser == webuserId && t.RefreshToken == refreshToken);

if (webuserToken == null)
throw new SecurityTokenException("Invalid token.");

if (webuserToken.RefreshToken != refreshToken)
throw new SecurityTokenValidationException("Invalid token.");

if (webuserToken.CreatedOn.AddHours(webuserToken.ExpiresIn) < DateTime.Now)
throw new SecurityTokenExpiredException("Token expired.");

var newAccessToken = GenerateAccessToken(webuserToken.User, applicationName, issuer, secretKey);
webuserToken.TokenForAccess = newAccessToken;
Context.SaveChanges();

return newAccessToken;
}

private Guid GetCallDispatcherIdFromExpiredToken(string token, string issuer, string appName, string secretKey)
{
var principal = GetPrincipalFromExpiredToken(token, issuer, appName, secretKey);
var id = principal.Claims.FirstOrDefault(claim => claim.Type == JwtRegisteredClaimNames.Sid)?.Value;
if (Guid.TryParse(id, out Guid callDispatcherId))
return callDispatcherId;
return Guid.Empty;
}

private ClaimsPrincipal GetPrincipalFromExpiredToken(string token, string issuer, string appName, string secretKey)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
ValidateIssuer = true,
ValidIssuer = issuer,
ValidateAudience = true,
ValidAudience = appName,
ValidateLifetime = false,
ClockSkew = TimeSpan.Zero
};

var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
if (!(securityToken is JwtSecurityToken jwtSecurityToken)
|| !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
throw new SecurityTokenException("Invalid token");

return principal;
}

protected string GenerateAccessToken(Webuser userLoggedIn, string applicationName, string issuer, string secretKey)
{
var claims = new[]
{
Expand All @@ -52,7 +106,18 @@ protected JwtSecurityToken GenerateJwtToken(Webuser userLoggedIn, string applica
claims,
expires: DateTime.UtcNow.AddMinutes(60),
signingCredentials: creds);
return token;

return new JwtSecurityTokenHandler().WriteToken(token);
}

private string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}
}
}
26 changes: 24 additions & 2 deletions Survi.Prevention.WebApi/Controllers/AuthentificationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using Survi.Prevention.Models.DataTransfertObjects;
using Survi.Prevention.ServiceLayer.Services;

namespace Survi.Prevention.WebApi.Controllers
Expand All @@ -23,9 +25,9 @@ public AuthentificationController(AuthentificationService service, IConfiguratio
}

[Route("[Action]"), HttpPost]
public ActionResult Logon(string user, string password)
public ActionResult Logon([FromBody]LoginInformations login)
{
var result = service.Login(user, password, applicationName, issuer, secretKey);
var result = service.Login(login.Username, login.Password, applicationName, issuer, secretKey);
if (result.user == null || result.token == null)
return Unauthorized();

Expand All @@ -45,6 +47,26 @@ public ActionResult Logon(string user, string password)
});
}

[Route("[Action]"), HttpPost, AllowAnonymous]
public ActionResult Refresh([FromBody]TokenRefreshResult tokens)
{
try
{
var newAccessToken = service.Refresh(tokens.AccessToken, tokens.RefreshToken, applicationName, issuer, secretKey);
return Ok(new { AccessToken = newAccessToken, tokens.RefreshToken });
}
catch (SecurityTokenExpiredException)
{
HttpContext.Response.Headers.Add("Refresh-Token-Expired", "true");
}
catch (SecurityTokenException)
{
HttpContext.Response.Headers.Add("Token-Invalid", "true");
}

return Unauthorized();
}

[HttpGet, Route("SessionStatus"), Authorize]
[ProducesResponseType(200)]
[ProducesResponseType(401)]
Expand Down
27 changes: 26 additions & 1 deletion Survi.Prevention.WebApi/TokenAuthentificationExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -27,11 +28,35 @@ public static IServiceCollection AddTokenAuthentification(this IServiceCollectio
{
config.RequireHttpsMetadata = false;
config.SaveToken = true;
config.TokenValidationParameters = GetAuthentificationParameters(secretKey, issuer, appName);
config.TokenValidationParameters = GetAuthentificationParameters(secretKey, issuer, appName);
config.Events = new JwtBearerEvents {OnAuthenticationFailed = AddTokenExpiredHeaderForTokenException};
});
ChangeRedirectionForUnauthorized(services);
return services;
}

private static Task AddTokenExpiredHeaderForTokenException(AuthenticationFailedContext context)
{
context.Response.StatusCode = 401;
if (context.Exception.GetType() == typeof(SecurityTokenException))
context.Response.Headers.Add("Token-Expired", "true");
else if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
context.Response.Headers.Add("Token-Expired", "true");
return Task.CompletedTask;
}

private static void ChangeRedirectionForUnauthorized(IServiceCollection services)
{
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
}

private static TokenValidationParameters GetAuthentificationParameters(string secretKey, string issuer, string appName)
{
var tokenValidationParameters = new TokenValidationParameters
Expand Down

0 comments on commit b3ac054

Please sign in to comment.