ASP .NET Web Api create + validate PASETO token

100 views Asked by At

I've tried all sorts of things, but I can't seem to figure out a way to work out how to make the token creator in PasetoService.cs work in harmony with the token validator in Startup.cs. It also gives a weird URL (http://localhost:8000/Account/Login?ReturnUrl=%2Ffruits) when I define the paseto authentication method in startup.cs.

I tried the following:

PasetoService.cs

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using PasetoAuth.Entities;
using PasetoAuth.Models;
using ScottBrady.IdentityModel.Crypto;
using ScottBrady.IdentityModel.Tokens;
using ScottBrady.IdentityModel.Tokens.Paseto;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;

namespace PasetoAuth.Controllers;

public class PasetoService : IPasetoService
{
    private readonly UserManager<PasetoUser> userManager;
    private readonly RoleManager<IdentityRole> roleManager;
    private readonly IConfiguration _configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();

    public PasetoService(UserManager<PasetoUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration configuration)
    {
        this.userManager = userManager;
        this.roleManager = roleManager;
        _configuration = configuration;
    }

    public async Task<(int, string)> Registration(PasetoRegistrationModel model, string role)
    {
        var userExists = await userManager.FindByNameAsync(model.Username);
        if (userExists != null)
            return (0, "User already exists");

        PasetoUser user = new()
        {
            Email = model.Email,
            SecurityStamp = Guid.NewGuid().ToString(),
            UserName = model.Username,
            Name = model.Name
        };
        var createUserResult = await userManager.CreateAsync(user, model.Password);
        if (!createUserResult.Succeeded)
            return (0, "User creation failed! Please check user details and try again.");

        if (!await roleManager.RoleExistsAsync(role))
            await roleManager.CreateAsync(new IdentityRole(role));

        if (await roleManager.RoleExistsAsync(PasetoUserRoles.User))
            await userManager.AddToRoleAsync(user, role);

        return (1, "User created successfully!");
    }

    public async Task<(int, string)> Login(PasetoLoginModel model)
    {
        var user = await userManager.FindByNameAsync(model.Username);
        if (user == null)
            return (0, "Invalid username");
        if (!await userManager.CheckPasswordAsync(user, model.Password))
            return (0, "Invalid password");

        var userRoles = await userManager.GetRolesAsync(user);
        var claims = new List<Claim>
        {
           new Claim(ClaimTypes.Name, user.UserName),
           new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        };

        foreach (var userRole in userRoles)
        {
            claims.Add(new Claim(ClaimTypes.Role, userRole));
        }
        string token = GenerateToken(claims);
        return (1, token);
    }

    private string GenerateToken(List<Claim> claims)
    {
        var handler = new PasetoTokenHandler();
        var privateKey = Base64UrlEncoder.DecodeBytes(_configuration["Token:PrivateKey"]);
        Dictionary<string, object> claimsDictionary = new Dictionary<string, object>();

        foreach (Claim claim in claims)
        {
            claimsDictionary.Add(claim.Type, claim.Value);
        }

        string token = handler.CreateToken(new PasetoSecurityTokenDescriptor(PasetoConstants.Versions.V2, PasetoConstants.Purposes.Public)
        {
            Issuer = _configuration["Token:ValidIssuer"],
            Audience = _configuration["Token:ValidAudience"],
            Expires = DateTime.UtcNow.AddHours(1),
            NotBefore = DateTime.UtcNow,
            Claims = claimsDictionary,
            SigningCredentials = new SigningCredentials(new EdDsaSecurityKey(EdDsa.Create(
                new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) { D = privateKey })), ExtendedSecurityAlgorithms.EdDsa)
        });

        return token;
    }
}

Startup.cs

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Paseto;
using PasetoAuth.Controllers;
using PasetoAuth.Entities;
using PasetoAuth.Models;
using PasetoBearer.Authentication;

namespace PasetoTestApi;

public class Startup
{
    public IConfiguration Configuration { get; }
    public Startup(IConfiguration configuration) { Configuration = configuration; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddDbContext<PasetoContext>(options => options.UseSqlServer(Configuration.GetConnectionString("PasetoServer")));
        services.AddIdentity<PasetoUser, IdentityRole>().AddEntityFrameworkStores<PasetoContext>().AddDefaultTokenProviders();

        services.AddAuthentication(PasetoBearerDefaults.AuthenticationScheme)
            .AddPasetoBearer(options =>
            {
                options.PublicKey = Base64UrlEncoder.DecodeBytes(Configuration["Token:PublicKey"]);
                options.PasetoTokenValidationParameters = new PasetoTokenValidationParameters()
                {
                    ValidAudience = Configuration["Token:ValidAudience"],
                    ValidIssuer = Configuration["Token:ValidIssuer"],
                };
                options.Validate();
            });

        services.AddTransient<IPasetoService, PasetoService>();
    }
    public void Configure(WebApplication app)
    {
        if (!app.Environment.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.UseHttpsRedirection();
        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();
    }
}

appsettings.json

{
    "ConnectionStrings": {
        "PasetoServer": "Server=BRCACERV3571G01\\SQLEXPRESS01;Database=PasetoDb;User Id=sa;Password=xs2me$cite;TrustServerCertificate=True;"
    },
    "Token": {
        "ValidAudience": "https://localhost:8000",
        "ValidIssuer": "https://localhost:8000",
        "PrivateKey": "YourPrivateKey",
        "PublicKey": "YourPublicKey"
    },
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    },
    "AllowedHosts": "*"
}
0

There are 0 answers