AUTH4D-139 application logo
Each public application (browser or native) can have one logo. The hosted login, guest upgrade and
device approval pages show it next to the application name, with the application name as its
alt text. Verification emails are unchanged. Logos are stored in application_logos (migration 0014) and are deleted with their application’s tenant.
Rules
- PNG or WebP only. The type is decided by the file’s magic bytes, which must match the request’s
Content-Type. SVG and every other format is rejected. - At most 65536 bytes (64 KB) and at most 512 by 512 pixels, read from the PNG
IHDRchunk or the WebPVP8X,VP8LorVP8header. A PNG must end with itsIENDchunk and a WebP’s RIFF length must match the body.
Upload
PUT /management/tenants/{tenantId}/clients/{clientId}/logo (scope client:write, project role
owner or admin; a viewer gets 403 insufficient_role)
The body is the raw image, sent with Content-Type: image/png or image/webp. These are the only
non-JSON management bodies the API boundary accepts. The response is sent with no-store:
{
"logo": {
"content_type": "image/png",
"byte_length": 2048,
"width": 256,
"height": 256,
"version": "<base64url SHA-256 of the image, 43 characters>",
"updated_at": "2026-10-06T12:00:00.000Z"
}
}
The upload replaces any earlier logo. Uploading the stored image again changes nothing and writes no second audit event. Failures write nothing:
| Status | Code | Cause |
|---|---|---|
| 415 | unsupported_media_type |
The Content-Type is not image/png or image/webp (for example SVG) |
| 413 | request_too_large |
The body is larger than 65536 bytes |
| 415 | logo_unsupported_type |
The bytes are not a PNG or WebP file |
| 400 | logo_type_mismatch |
The bytes are PNG but the Content-Type says WebP, or the reverse |
| 400 | logo_malformed |
The image header or trailer cannot be read |
| 400 | logo_dimensions |
The image is wider or taller than 512 pixels |
| 400 | logo_empty |
The body is empty |
Management errors use the AUTH4D-18 error shape, with details[].field set to logo.
Preview and removal
GET /management/tenants/{tenantId}/clients/{clientId}/logo(scopeclient:read, any project role) returns the stored image with itsContent-Type,X-Content-Type-Options: nosniffandno-store, or404 not_foundwhen the application has no logo. The console shows it through the BFF.DELETE /management/tenants/{tenantId}/clients/{clientId}/logo(scopeclient:write, owner or admin) removes the logo and returns{"logo": null}. Removing an absent logo also returns that.- The application document (
GETorPATCH .../clients/{clientId}, see AUTH4D-56 (repository filedocs/api-spec/auth4d-56.md)) gains"logo": the metadata above, ornull.
Through the console BFF these routes are /api/management/... and, like every BFF write, upload
and removal require the console Origin (CSRF) and the session cookie.
Audit
Upload and removal each write one management.client.presentation.updated event for the client,
with changed_fields: "branding.logo" and logo: "uploaded" (plus content_type) or
logo: "removed". The image, its digest and its size are never written to the audit log.
Public logo
GET /t/{tenantId}/clients/{clientId}/logo?v={version} on the authentication origin serves the
logo of an enabled browser or native application of an active customer tenant. Anything else, a
v that is not the current version, or an unknown query parameter returns 404 with no-store.
| Header | Value |
|---|---|
Content-Type |
image/png or image/webp, as stored |
X-Content-Type-Options |
nosniff |
Cache-Control |
public, max-age=31536000, immutable with the current v; public, max-age=0, must-revalidate without v |
ETag |
"{version}"; a matching If-None-Match returns 304 |
Content-Security-Policy |
default-src 'none'; sandbox |
Cross-Origin-Resource-Policy |
same-origin |
A new upload changes the version, so a replaced logo is never served from an old URL.
Hosted pages
GET /t/{tenantId}/login/context and GET /device/verify add branding.logo_url when the
application has a logo. It is the same-origin path above, for example
/t/ten_…/clients/cli_…/logo?v=… (ApplicationLogoPathSchema in packages/contracts). The field
is absent otherwise, so earlier responses are unchanged. The hosted login only uses a path whose
tenant and client match the page’s own. The hosted-login Content Security Policy is unchanged
(img-src 'self').
Source: docs/api-spec/auth4d-139.md