using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading.Tasks;
using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Services.Token;
using Timeline.Services.User;
namespace Timeline.Auth
{
public static class AuthenticationConstants
{
public const string Scheme = "Bearer";
public const string DisplayName = "My Jwt Auth Scheme";
public const string PermissionClaimName = "Permission";
}
public class MyAuthenticationOptions : AuthenticationSchemeOptions
{
///
/// The query param key to search for token. If null then query params are not searched for token. Default to "token".
///
public string TokenQueryParamKey { get; set; } = "token";
}
public class MyAuthenticationHandler : AuthenticationHandler
{
private const string TokenErrorCodeKey = "TokenErrorCode";
private static int GetErrorCodeForUserTokenException(UserTokenException e)
{
return e switch
{
UserTokenTimeExpiredException => ErrorCodes.Common.Token.TimeExpired,
UserTokenVersionExpiredException => ErrorCodes.Common.Token.VersionExpired,
UserTokenBadFormatException => ErrorCodes.Common.Token.BadFormat,
UserTokenUserNotExistException => ErrorCodes.Common.Token.UserNotExist,
_ => ErrorCodes.Common.Token.Unknown
};
}
private static string GetTokenErrorMessageFromErrorCode(int errorCode)
{
return errorCode switch
{
ErrorCodes.Common.Token.TimeExpired => Resource.MessageTokenTimeExpired,
ErrorCodes.Common.Token.VersionExpired => Resource.MessageTokenVersionExpired,
ErrorCodes.Common.Token.BadFormat => Resource.MessageTokenBadFormat,
ErrorCodes.Common.Token.UserNotExist => Resource.MessageTokenUserNotExist,
_ => Resource.MessageTokenUnknownError
};
}
private readonly ILogger _logger;
private readonly IUserTokenManager _userTokenManager;
private readonly IUserPermissionService _userPermissionService;
private readonly IOptionsMonitor _jsonOptions;
public MyAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserTokenManager userTokenManager, IUserPermissionService userPermissionService, IOptionsMonitor jsonOptions)
: base(options, logger, encoder, clock)
{
_logger = logger.CreateLogger();
_userTokenManager = userTokenManager;
_userPermissionService = userPermissionService;
_jsonOptions = jsonOptions;
}
// return null if no token is found
private string? ExtractToken()
{
// check the authorization header
string header = Request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
var token = header["Bearer ".Length..].Trim();
_logger.LogInformation(Resource.LogTokenFoundInHeader, token);
return token;
}
// check the query params
var paramQueryKey = Options.TokenQueryParamKey;
if (!string.IsNullOrEmpty(paramQueryKey))
{
string token = Request.Query[paramQueryKey];
if (!string.IsNullOrEmpty(token))
{
_logger.LogInformation(Resource.LogTokenFoundInQuery, paramQueryKey, token);
return token;
}
}
{
var token = Request.Query["access_token"];
var path = Context.Request.Path;
if (!string.IsNullOrEmpty(token) && path.StartsWithSegments("/api/hub"))
{
_logger.LogInformation(Resource.LogTokenFoundInQuery, "access_token", token);
return token;
}
}
// not found anywhere then return null
return null;
}
protected override async Task HandleAuthenticateAsync()
{
var token = ExtractToken();
if (string.IsNullOrEmpty(token))
{
_logger.LogInformation(Resource.LogTokenNotFound);
return AuthenticateResult.NoResult();
}
try
{
var user = await _userTokenManager.VerifyTokenAsync(token);
var identity = new ClaimsIdentity(AuthenticationConstants.Scheme);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64));
identity.AddClaim(new Claim(identity.NameClaimType, user.Username, ClaimValueTypes.String));
var permissions = await _userPermissionService.GetPermissionsOfUserAsync(user.Id);
identity.AddClaims(permissions.Select(permission => new Claim(AuthenticationConstants.PermissionClaimName, permission.ToString(), ClaimValueTypes.String)));
var principal = new ClaimsPrincipal();
principal.AddIdentity(identity);
return AuthenticateResult.Success(new AuthenticationTicket(principal, AuthenticationConstants.Scheme));
}
catch (UserTokenException e)
{
var errorCode = GetErrorCodeForUserTokenException(e);
_logger.LogInformation(e, Resource.LogTokenValidationFail, GetTokenErrorMessageFromErrorCode(errorCode));
return AuthenticateResult.Fail(e, new AuthenticationProperties(new Dictionary()
{
[TokenErrorCodeKey] = errorCode.ToString(CultureInfo.InvariantCulture)
}));
}
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.StatusCode = 401;
CommonResponse body;
if (properties.Items.TryGetValue(TokenErrorCodeKey, out var tokenErrorCode))
{
if (!int.TryParse(tokenErrorCode, out var errorCode))
errorCode = ErrorCodes.Common.Token.Unknown;
body = new CommonResponse(errorCode, GetTokenErrorMessageFromErrorCode(errorCode));
}
else
{
body = new CommonResponse(ErrorCodes.Common.Unauthorized, Resource.MessageNoToken);
}
var bodyData = JsonSerializer.SerializeToUtf8Bytes(body, typeof(CommonResponse), _jsonOptions.CurrentValue.JsonSerializerOptions);
Response.ContentType = MimeTypes.ApplicationJson;
Response.ContentLength = bodyData.Length;
await Response.Body.WriteAsync(bodyData);
}
}
}