Partner API Middleware
Partner API middleware handles versioning, access control, and audit logging for third-party integrations. Partners access data through versioned endpoints with OAuth scope-based permissions.
Middleware Aliases
Section titled “Middleware Aliases”| Alias | Class | Purpose |
|---|---|---|
partner.requests_logger | Partner\LogRequests | Audit logging for modifications |
partner.access_permission | Partner\CheckAccessPermission | Partner access validation |
partner.supported_versions | Partner\CheckEndpointVersion | API version compatibility |
Code Location
Section titled “Code Location”All partner middleware is in app/Http/Middleware/Partner/:
LogRequests.phpCheckAccessPermission.phpCheckEndpointVersion.php
1. CheckEndpointVersion
Section titled “1. CheckEndpointVersion”File: app/Http/Middleware/Partner/CheckEndpointVersion.php
Validates that the partner is using a supported API version for the endpoint.
Implementation
Section titled “Implementation”final class CheckEndpointVersion{ public function handle(Request $request, Closure $next, string ...$supported_versions): Response { $request_version = PartnerApiUtils::validateAndGetRequestVersion($request);
if (!in_array($request_version, $supported_versions, true)) { $supported_versions_string = implode(', ', $supported_versions); throw new UnprocessableEntityHttpException( "The rc-version [{$request_version}] is not available for this endpoint. Supported versions: [{$supported_versions_string}]" ); }
return $next($request); }}Key Behavior
Section titled “Key Behavior”- Uses
PartnerApiUtils::validateAndGetRequestVersion($request)to get and validate version - Throws
UnprocessableEntityHttpException(422) for unsupported versions - Compares against provided
$supported_versionsusing strict comparison
Available Versions
Section titled “Available Versions”final class PartnerApiUtils{ public const string VERSION_20210416 = '2021-04-16'; public const string VERSION_20230516 = '2023-05-16';
public const array AVAILABLE_VERSIONS = [ self::VERSION_20210416, self::VERSION_20230516, ];
private const string API_VERSION_LABEL = 'rc-version';
public static function validateAndGetRequestVersion(Request $request): string { // Validates rc-version header format and value $validator = Validator::make($request->headers->all(), [ self::API_VERSION_LABEL => 'date_format:Y-m-d|in:' . implode(',', self::AVAILABLE_VERSIONS), ]);
// Returns validated version or throws }}Required Header
Section titled “Required Header”rc-version: 2023-05-16Route Usage
Section titled “Route Usage”// All supported versionsRoute::group([ 'middleware' => 'partner.supported_versions:' . implode(',', PartnerApiUtils::AVAILABLE_VERSIONS)], function () { Route::get('households', [PartnerHouseholdController::class, 'index']);});
// Specific version onlyRoute::group([ 'middleware' => 'partner.supported_versions:' . PartnerApiUtils::VERSION_20230516], function () { Route::get('households/{household}/details', [PartnerHouseholdController::class, 'details']);});Error Response
Section titled “Error Response”{ "message": "The rc-version [2020-01-01] is not available for this endpoint. Supported versions: [2021-04-16, 2023-05-16]"}HTTP Status: 422 Unprocessable Entity
2. CheckAccessPermission
Section titled “2. CheckAccessPermission”File: app/Http/Middleware/Partner/CheckAccessPermission.php
Validates that the partner has permission to access the requested resource via OAuth client.
Implementation
Section titled “Implementation”final class CheckAccessPermission{ public function handle(Request $request, Closure $next): Response { $user = Auth::getUserOrFail(); $locality_advisor = LocalityManager::getAdvisorOrFail();
// Check if locality advisor matches authenticated user if ($locality_advisor->id !== $user->id) { return response(status: Response::HTTP_NOT_FOUND); }
$oauth_access_token = OauthAccessToken::findOrFail($user->token()->id); $oauth_client = $oauth_access_token->client;
// Check if advisor can use this OAuth client if (!$user->advisor->canUseOauthClient($oauth_client)) { throw new AccessDeniedHttpException("You don't have permission to use the API."); }
return $next($request); }}Key Behavior
Section titled “Key Behavior”- Uses
Auth::getUserOrFail()to get authenticated user - Uses
LocalityManager::getAdvisorOrFail()to get locality advisor - Returns 404 Not Found (not 403) if locality advisor doesn’t match user
- Gets OAuth client from
OauthAccessTokenmodel - Checks
$user->advisor->canUseOauthClient($oauth_client)for permission
Route Usage
Section titled “Route Usage”Route::group([ 'middleware' => ['auth:api', 'partner.access_permission']], function () { Route::get('advisors/{advisor}', [PartnerAdvisorController::class, 'show']); Route::get('households/{household}', [PartnerHouseholdController::class, 'show']);});Error Responses
Section titled “Error Responses”// OAuth client not allowed{ "message": "You don't have permission to use the API."}HTTP Status: 403 Forbidden
For locality mismatch: Empty response with 404 Not Found
3. LogRequests
Section titled “3. LogRequests”File: app/Http/Middleware/Partner/LogRequests.php
Logs partner API requests for audit purposes using FileLog package.
Implementation
Section titled “Implementation”final class LogRequests{ public function handle(Request $request, Closure $next): Response { $response = $next($request);
// Only log for partner host AND write operations if (str_starts_with($request->getHost(), 'partner') && in_array($request->method(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_DELETE], true)) { self::logRequest($request); }
return $response; }
private static function logRequest(Request $request): void { $bearer_token = $request->bearerToken();
if ($bearer_token === null) { throw new UnauthorizedHttpException('Bearer'); }
// Decode JWT to get user claims $user_claims = \Safe\json_decode( JWT::urlsafeB64Decode(explode('.', $bearer_token)[1]), true );
$log_data = [ 'advisor_id' => $user_claims['sub'], 'body' => $request->json()->all(), 'oauth_client_id' => $user_claims['aud'], 'method' => $request->method(), 'route_name' => $request->route()->getName(), 'version' => $request->header('rc-version'), ];
FileLog::setLocation('audit'); FileLog::write( ['partner_api', Crypt::decryptId($log_data['advisor_id']), $log_data['oauth_client_id'], Date::now()->format('Y-m-d')], $log_data['method'], $log_data, ); FileLog::resetLocation(); }}Key Behavior
Section titled “Key Behavior”- Only logs when host starts with ‘partner’ AND method is POST/PUT/DELETE
- Decodes JWT to extract user claims (sub, aud) - does NOT use
$request->user() - Uses FileLog package (not database model) for audit logging
- File path:
audit/partner_api/{decrypted_advisor_id}/{oauth_client_id}/{date}.log - Throws
UnauthorizedHttpExceptionif no bearer token
Log Fields
Section titled “Log Fields”| Field | Source | Description |
|---|---|---|
advisor_id | JWT sub claim | Encrypted advisor ID |
body | $request->json()->all() | Request body (raw, not sanitized) |
oauth_client_id | JWT aud claim | OAuth client ID |
method | $request->method() | HTTP method |
route_name | $request->route()->getName() | Named route |
version | rc-version header | API version |
Route Usage
Section titled “Route Usage”Route::group([ 'middleware' => ['auth:api', 'partner.requests_logger']], function () { Route::post('households', [PartnerHouseholdController::class, 'store']); Route::put('households/{household}', [PartnerHouseholdController::class, 'update']); Route::delete('households/{household}', [PartnerHouseholdController::class, 'destroy']);});4. Complete Partner Route Structure
Section titled “4. Complete Partner Route Structure”Route::prefix('partner/v1') ->middleware(['api', 'auth:api']) ->group(function () {
// Standard endpoints with version check Route::group([ 'middleware' => 'partner.supported_versions:' . implode(',', PartnerApiUtils::AVAILABLE_VERSIONS) ], function () {
// Read endpoints Route::middleware('scope:household:read')->group(function () { Route::get('households', [PartnerHouseholdController::class, 'index']); Route::get('households/{household}', [PartnerHouseholdController::class, 'show']); });
// Write endpoints (with logging and access check) Route::middleware([ 'scope:household:write', 'partner.access_permission', 'partner.requests_logger' ])->group(function () { Route::post('households', [PartnerHouseholdController::class, 'store']); Route::put('households/{household}', [PartnerHouseholdController::class, 'update']); }); }); });5. Partner OAuth Scopes
Section titled “5. Partner OAuth Scopes”Partners use OAuth scopes to define access levels:
| Scope | Description |
|---|---|
household:read | Read household data |
household:write | Create/update households |
advisor:read | Read advisor data |
account:read | Read account data |
Combining with Scope Middleware
Section titled “Combining with Scope Middleware”Route::middleware(['scope:household:read'])->group(function () { Route::get('households', [PartnerHouseholdController::class, 'index']);});
Route::middleware(['scopes:household:read,account:read'])->group(function () { // Requires BOTH scopes Route::get('households/{household}/full', [PartnerHouseholdController::class, 'showFull']);});6. Partner Middleware Stack
Section titled “6. Partner Middleware Stack”Request with rc-version header │ ▼┌────────────────────────┐│ CheckEndpointVersion │────Invalid Version────▶ 422│ (PartnerApiUtils) │└────────────────────────┘ │ ▼┌────────────────────────┐│ OAuth Scope Check │────Missing Scope────▶ 403└────────────────────────┘ │ ▼┌────────────────────────┐│ CheckAccessPermission │────Mismatch────▶ 404│ (locality + OAuth) │────No Permission────▶ 403└────────────────────────┘ │ ▼┌────────────────────────┐│ LogRequests │ (POST/PUT/DELETE + partner host)│ (FileLog + JWT decode) │└────────────────────────┘ │ ▼ ControllerSummary
Section titled “Summary”| Middleware | Purpose | Error Code | Notes |
|---|---|---|---|
partner.supported_versions | Validate API version | 422 | Uses PartnerApiUtils |
partner.access_permission | Validate OAuth client access | 403/404 | LocalityManager + canUseOauthClient |
partner.requests_logger | Audit logging (writes) | - | FileLog, JWT decode, partner host only |