PhotoFlow Documentation Help

Authorization & Security

PhotoFlow implements role-based authorization using ASP.NET Core policies with JWT authentication.

Authentication Flow

GoogleIdentityAPIClientGoogleIdentityAPIClientLogin Request (Google OAuth)Validate OAuth TokenUser InfoCreate/Update UserJWT Token + Refresh TokenRequest + Bearer TokenValidate JWTCheck Authorization PolicyResponse

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 }

Security Headers

CORS Configuration

services.AddCors(options => { options.AddPolicy("PhotoFlowCors", policy => { policy.WithOrigins( "https://app.photoflow.vn", "https://admin.photoflow.vn" ) .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); }); });

Security Headers Middleware

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

GoogleBackendFrontendUserGoogleBackendFrontendUserClick "Login with Google"OAuth RedirectConsentAuthorization CodePOST /auth/google (code)Exchange Code for TokenAccess Token + ID TokenCreate/Update UserJWT Token

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