Authorization & Security
PhotoFlow implements role-based authorization using ASP.NET Core policies with JWT authentication.
Authentication Flow
JWT Token Structure
Token Claims
Claim | Description | Example |
|---|
sub
| User ID | 550e8400-e29b-41d4-a716-446655440000
|
email
| User email | user@company.com
|
name
| Display name | John Doe
|
role
| User role(s) | photographer
|
org_id
| Organization ID | org-123
|
iat
| Issued at | Unix timestamp |
exp
| Expiration | Unix timestamp |
Token Validation
// Program.cs
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = configuration["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = configuration["Jwt:Audience"],
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration["Jwt:Secret"])),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
Authorization Policies
PhotoFlow defines 5 authorization policies in BuildingBlocks.Authentication:
Policy Definitions
// AuthorizationPolicies.cs
public static class AuthorizationPolicies
{
public const string Photographer = "Photographer";
public const string Admin = "Admin";
public const string SuperAdmin = "SuperAdmin";
public const string Anonymous = "Anonymous";
public const string PhotographerOrAnonymous = "PhotographerOrAnonymous";
public static IServiceCollection AddPhotoFlowAuthorizationPolicies(
this IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.AddPolicy(Photographer, policy =>
policy.RequireRole("photographer", "org_admin"));
options.AddPolicy(Admin, policy =>
policy.RequireRole("org_admin"));
options.AddPolicy(SuperAdmin, policy =>
policy.RequireRole("supper_admin"));
options.AddPolicy(Anonymous, policy =>
policy.RequireRole("anonymous"));
options.AddPolicy(PhotographerOrAnonymous, policy =>
policy.RequireRole("photographer", "org_admin", "anonymous"));
});
return services;
}
}
Policy Matrix
Policy | photographer | org_admin | supper_admin | anonymous |
|---|
Photographer | ✅ | ✅ | ❌ | ❌ |
Admin | ❌ | ✅ | ❌ | ❌ |
SuperAdmin | ❌ | ❌ | ✅ | ❌ |
Anonymous | ❌ | ❌ | ❌ | ✅ |
PhotographerOrAnonymous | ✅ | ✅ | ❌ | ✅ |
Role Definitions
Role | Description | Typical Users |
|---|
photographer
| Standard photographer | Company photographers |
org_admin
| Organization admin | Studio managers |
supper_admin
| System super admin | Platform admins |
anonymous
| Public anonymous user | External clients viewing albums |
Usage Examples
Controller-Level Authorization
[ApiController]
[Route("api/v1/photos")]
[Authorize(Policy = "Photographer")]
public class PhotoController : ControllerBase
{
// All endpoints require Photographer policy
[HttpGet]
public async Task<IActionResult> GetPhotosAsync() { }
}
Endpoint-Level Authorization
[ApiController]
[Route("api/v1/albums")]
public class AlbumController : ControllerBase
{
// Photographers and admins can create
[HttpPost]
[Authorize(Policy = "Photographer")]
public async Task<IActionResult> CreateAlbumAsync() { }
// Only admins can close
[HttpPost("{id}/close")]
[Authorize(Policy = "Admin")]
public async Task<IActionResult> CloseAlbumAsync() { }
// Anonymous clients can confirm feedback
[HttpPost("{id}/confirm-feedback")]
[Authorize(Policy = "PhotographerOrAnonymous")]
public async Task<IActionResult> ConfirmFeedbackAsync() { }
}
Album Workflow Authorization
Transition | Policy | Reason |
|---|
Create Album | Photographer | Only photographers create albums |
SyncRaw | Photographer | Internal operation |
Rate | Photographer | Internal operation |
Retouching | Photographer | Internal operation |
FinishRetouch | Photographer | Internal operation |
RequestRevision | PhotographerOrAnonymous | Clients can request revisions |
ConfirmFeedback | PhotographerOrAnonymous | Clients confirm selections |
Close | Admin | Admin-only operation |
Anonymous Access
Album Password Authentication
Anonymous users access albums via password:
// AnonymousAuthenticationHandler.cs
public class AnonymousAuthController : ControllerBase
{
[HttpPost("albums/{albumId}/verify")]
[AllowAnonymous]
public async Task<IActionResult> VerifyAlbumPasswordAsync(
Guid albumId,
[FromBody] VerifyPasswordRequest request)
{
var album = await _albumService.GetByIdAsync(albumId);
if (!PasswordHasher.Verify(request.Password, album.HashedPassword))
return Unauthorized();
var claims = new[]
{
new Claim(ClaimTypes.Role, "anonymous"),
new Claim("album_id", albumId.ToString())
};
var token = _tokenService.GenerateToken(claims);
return Ok(new { Token = token });
}
}
Anonymous Token Claims
{
"role": "anonymous",
"album_id": "550e8400-e29b-41d4-a716-446655440000",
"exp": 1234567890
}
CORS Configuration
services.AddCors(options =>
{
options.AddPolicy("PhotoFlowCors", policy =>
{
policy.WithOrigins(
"https://app.photoflow.vn",
"https://admin.photoflow.vn"
)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
app.Use(async (context, next) =>
{
context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
context.Response.Headers.Append("X-Frame-Options", "DENY");
context.Response.Headers.Append("X-XSS-Protection", "1; mode=block");
context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
await next();
});
Rate Limiting
services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.User.Identity?.Name ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous",
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
});
Google OAuth Integration
OAuth Flow
Configuration
services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = configuration["Google:ClientId"];
options.ClientSecret = configuration["Google:ClientSecret"];
options.Scope.Add("email");
options.Scope.Add("profile");
});
Troubleshooting
Common Issues
Issue | Cause | Solution |
|---|
401 Unauthorized | Missing/invalid token | Check token expiration, format |
403 Forbidden | Wrong role | Verify user role matches policy |
CORS error | Origin not allowed | Add origin to CORS config |
Token expired | Session timeout | Implement token refresh |
Debug Logging
// Enable detailed auth logging
services.AddLogging(builder =>
{
builder.AddFilter("Microsoft.AspNetCore.Authentication", LogLevel.Debug);
builder.AddFilter("Microsoft.AspNetCore.Authorization", LogLevel.Debug);
});
API Overview
Architecture
Album Workflow
Last modified: 20 December 2025