Skip to main content

Overview

The XGP Photo API uses JWT (JSON Web Tokens) for authentication combined with a client credentials validation system. This provides a two-layer security approach:
  1. Client validation - Validates the client application using ClientId and ClientSecret
  2. User authentication - Validates user credentials and generates JWT tokens
  3. Role-based authorization - Controls access to endpoints based on user roles

Authentication Flow

1

Client Registration

Register your client application in appsettings.json:
"AuthClients": [
  {
    "ClientId": "xgp-web",
    "ClientSecret": "Y0urCl13ntS3cret!2025",
    "Description": "Frontend Web principal"
  },
  {
    "ClientId": "xgp-mobile",
    "ClientSecret": "Mob1leAppS3cret@2025",
    "Description": "Aplicación móvil"
  }
]
2

Login Request

Send a POST request to /api/Auth/login with user and client credentials:
{
  "email": "[email protected]",
  "password": "XgpPhoto!2025$Secure",
  "clientId": "xgp-web",
  "clientSecret": "Y0urCl13ntS3cret!2025"
}
3

Receive JWT Token

On successful authentication, you’ll receive a JWT token:
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
4

Use Token in Requests

Include the token in the Authorization header:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  https://api.xgp.com/api/Projects

JWT Configuration

JWT settings are configured in appsettings.json:
"Jwt": {
  "Issuer": "XgpPhotoApi",
  "Audience": "XgpPhotoClients",
  "Key": "clave-super-segura-y-larga-para-firmar-jwt",
  "ExpMinutes": 60
}
Never commit your JWT secret key to version control. Use environment variables or secure configuration management in production.

JWT Options Class

The JWT configuration is mapped to the JwtOptions class:
Infrastructure/Identity/JwtOptions.cs
public class JwtOptions
{
    public string Issuer { get; set; } = default!;
    public string Audience { get; set; } = default!;
    public string Key { get; set; } = default!;
    public int ExpMinutes { get; set; } = 60;
}

Token Generation

The JwtTokenService class handles JWT token creation with user claims and roles:
Infrastructure/Identity/JwtTokenService.cs
public class JwtTokenService : ITokenService
{
    private readonly JwtOptions _options;
    private readonly UserManager<IdentityUser> _userManager;

    public async Task<string> CreateTokenAsync(IdentityUser user)
    {
        var roles = await _userManager.GetRolesAsync(user);

        var claims = new List<Claim>
        {
            new(JwtRegisteredClaimNames.Sub, user.Id),
            new(JwtRegisteredClaimNames.Email, user.Email ?? ""),
            new(ClaimTypes.Name, user.UserName ?? "")
        };

        // Add both claim types for maximum compatibility
        claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));
        claims.AddRange(roles.Select(r => new Claim("role", r)));

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Key));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: _options.Issuer,
            audience: _options.Audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(_options.ExpMinutes),
            signingCredentials: creds
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}
The service adds roles in two formats (ClaimTypes.Role and "role") to ensure compatibility with both .NET standard and plain JWT parsers.

Client Validation

Before authenticating users, the API validates the client application credentials:
Infrastructure/Identity/AuthClientValidator.cs
public class AuthClientValidator : IAuthClientValidator
{
    private readonly IReadOnlyList<AuthClient> _clients;

    public AuthClientValidator(IOptions<List<AuthClient>> options)
    {
        _clients = options.Value;
    }

    public bool Validate(string clientId, string clientSecret)
    {
        if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret))
            return false;

        return _clients.Any(c =>
            c.ClientId.Equals(clientId, StringComparison.OrdinalIgnoreCase) &&
            c.ClientSecret == clientSecret);
    }
}

Authentication Service

The AuthService orchestrates the complete authentication flow:
Application/Services/AuthService.cs
public async Task<AuthResponseDto?> AuthenticateAsync(LoginDto dto)
{
    var clientID = dto.ClientId.Trim();
    var clientSecret = dto.ClientSecret.Trim();

    // Step 1: Validate client credentials
    if (!_clientValidator.Validate(clientID, clientSecret))
        return null;

    // Step 2: Find user by email
    var user = await _userManager.FindByEmailAsync(dto.Email.ToLower());
    if (user == null)
        return null;

    // Step 3: Check password
    var result = await _signInManager.CheckPasswordSignInAsync(user, dto.Password, false);
    if (!result.Succeeded)
        return null;

    // Step 4: Generate JWT token
    var token = await _tokenService.CreateTokenAsync(user);
    var roles = await _userManager.GetRolesAsync(user);

    return new AuthResponseDto
    {
        Token = token
    };
}

Auth Controller

The login endpoint is exposed through the AuthController:
Api/Controllers/AuthController.cs
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly IAuthService _authService;
    private readonly ILogger<AuthController> _logger;

    [HttpPost("login")]
    [AllowAnonymous]
    public async Task<IActionResult> Login([FromBody] LoginDto dto)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);

        var result = await _authService.AuthenticateAsync(dto);
        if (result == null)
        {
            _logger.LogWarning("Intento de login fallido para {Email}", dto.Email);
            return Unauthorized("Credenciales inválidas o cliente no autorizado.");
        }

        _logger.LogInformation("Usuario {Email} autenticado correctamente.", dto.Email);
        return Ok(result);
    }
}

Role-Based Authorization

Protecting Endpoints

Use the [Authorize] attribute with roles to restrict access:
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Create([FromBody] ProjectCreateDto dto)
{
    var result = await _service.CreateAsync(dto);
    return CreatedAtAction(nameof(GetAll), new { id = result.Id }, result);
}

Available Role Configurations

[Authorize(Roles = "Admin")]
Only users with the Admin role can access this endpoint.
[Authorize(Roles = "Admin,Editor")]
Users with either Admin or Editor role can access this endpoint.
[Authorize(Roles = "Admin")]
[Authorize(Roles = "Manager")]
Only users with both Admin and Manager roles can access this endpoint.
[AllowAnonymous]
No authentication required.

Service Registration

Register authentication services in your dependency injection container:
Infrastructure/Extensions/AuthenticationExtensions.cs
public static IServiceCollection AddAuthInfrastructure(
    this IServiceCollection services, 
    IConfiguration config)
{
    services.Configure<JwtOptions>(config.GetSection("Jwt"));
    services.Configure<List<AuthClient>>(config.GetSection("AuthClients"));

    services.AddScoped<IAuthClientValidator, AuthClientValidator>();
    services.AddScoped<ITokenService, JwtTokenService>();
    services.AddScoped<IAuthService, AuthService>();

    return services;
}

Testing Authentication

Using cURL

# Login
curl -X POST http://localhost:5000/api/Auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "XgpPhoto!2025$Secure",
    "clientId": "xgp-web",
    "clientSecret": "Y0urCl13ntS3cret!2025"
  }'

# Use token in subsequent requests
curl -X GET http://localhost:5000/api/Projects \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

Using Postman

1

Create Login Request

  • Method: POST
  • URL: http://localhost:5000/api/Auth/login
  • Body: Raw JSON with email, password, clientId, and clientSecret
2

Save Token

Copy the token from the response
3

Add to Authorization

  • Type: Bearer Token
  • Token: Paste your JWT token

Security Best Practices

Production Security Checklist
  • Use environment variables for sensitive configuration
  • Implement HTTPS/TLS for all API endpoints
  • Use strong, randomly generated keys (at least 256 bits)
  • Implement token refresh mechanism
  • Add rate limiting to login endpoint
  • Log authentication failures for security monitoring
  • Implement account lockout after failed attempts
  • Use secure password hashing (ASP.NET Identity handles this)

Common Issues

Possible causes:
  • Invalid or expired JWT token
  • Missing Authorization header
  • Incorrect bearer token format
Solution: Verify token is valid and properly formatted: Bearer YOUR_TOKEN
Possible causes:
  • User doesn’t have required role
  • Token is valid but lacks necessary permissions
Solution: Check user roles in database and endpoint role requirements
Possible causes:
  • ClientId or ClientSecret mismatch
  • Client not registered in appsettings.json
Solution: Verify client credentials match configuration exactly

Next Steps

Working with Projects

Learn how to manage photography projects

Database Setup

Configure PostgreSQL and run migrations

Build docs developers (and LLMs) love