navidrome/api/bundled/openapi.yaml
Deluan d1b876097f refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.

Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".

Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.

Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.

Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00

736 lines
24 KiB
YAML

openapi: 3.0.3
info:
title: Navidrome API
version: 1.0.0
description: |
Navidrome API v1. Spec-first, additive within v1. Clients discover implemented
capability modules through `GET /capabilities` and never sniff versions.
Enums are open: new values may be added to any enum within v1. Clients must
accept values they do not recognise instead of failing.
Every operation declares `x-stability-level`: `alpha` operations may change or
disappear without notice, `beta` and `stable` operations only change additively.
A level is only ever raised, never lowered.
`HEAD` is accepted wherever `GET` is. A `405` response lists the allowed methods
in its `Allow` header.
Operations that need a grant declare `security: [{bearerAuth: []}]` and the scope they need in
`x-scope` (OpenAPI 3.0 does not allow scopes on bearer schemes). Clients send the grant secret as
`Authorization: Bearer <secret>`. A revoked grant stops working within one minute at most.
license:
name: GPL-3.0
url: https://www.gnu.org/licenses/gpl-3.0.html
servers:
- url: /api/v1
tags:
- name: server
description: Server discovery and the published OpenAPI document.
- name: auth
description: Grants and login methods.
paths:
/server:
get:
operationId: getServerInfo
x-module: core
x-stability-level: alpha
tags: [server]
security: []
summary: Describe the server
description: |
Returns the public server description. No authentication required.
Capability modules are listed by `GET /capabilities`.
responses:
'200':
description: Server description.
content:
application/json:
schema:
$ref: '#/components/schemas/ServerInfo'
'500':
$ref: '#/components/responses/InternalError'
/capabilities:
get:
operationId: getCapabilities
x-module: core
x-stability-level: alpha
tags: [server]
summary: List implemented capability modules
description: The capability modules this server implements. Any valid grant may read it, whatever its scopes.
security: [{bearerAuth: []}]
responses:
'200':
description: Implemented modules.
content:
application/json:
schema: {$ref: '#/components/schemas/Capabilities'}
'401': {$ref: '#/components/responses/Unauthorized'}
'500': {$ref: '#/components/responses/InternalError'}
/openapi.json:
get:
operationId: getOpenAPISpecJSON
x-module: core
x-stability-level: alpha
tags: [server]
security: []
summary: Get the OpenAPI document (JSON)
description: The bundled OpenAPI document of the running server version. Supports ETag revalidation.
responses:
'200':
description: The OpenAPI document.
headers:
ETag:
$ref: '#/components/headers/ETag'
content:
application/json:
schema:
type: object
description: OpenAPI 3.0 document.
'304':
$ref: '#/components/responses/NotModified'
/auth/grants:
get:
operationId: listGrants
x-module: core
x-scope: read
x-stability-level: alpha
tags: [auth]
summary: List my grants
description: "The caller's grants, most recently used first. Grants idle long enough to have expired are not listed."
security: [{bearerAuth: []}]
parameters:
- $ref: '#/components/parameters/offset'
- $ref: '#/components/parameters/limit'
responses:
'200':
description: A page of grants.
content:
application/json:
schema:
$ref: '#/components/schemas/GrantList'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalError'
/openapi.yaml:
get:
operationId: getOpenAPISpecYAML
x-module: core
x-stability-level: alpha
tags: [server]
security: []
summary: Get the OpenAPI document (YAML)
description: The bundled OpenAPI document of the running server version. Supports ETag revalidation.
responses:
'200':
description: The OpenAPI document.
headers:
ETag:
$ref: '#/components/headers/ETag'
content:
application/yaml:
schema:
type: object
description: OpenAPI 3.0 document.
'304':
$ref: '#/components/responses/NotModified'
/auth/grants/{id}:
delete:
operationId: revokeGrant
x-module: core
x-scope: read
x-stability-level: alpha
tags: [auth]
summary: Revoke one of my grants
description: "Revokes the grant; requests with its secret fail from then on. Another user's grant id answers 404."
security: [{bearerAuth: []}]
parameters:
- name: id
in: path
required: true
description: Grant id.
schema:
type: string
maxLength: 64
responses:
'204':
description: Revoked.
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalError'
/auth/logout:
post:
operationId: logout
x-module: core
x-scope: read
x-stability-level: alpha
tags: [auth]
summary: Log out
description: Revokes the grant that made this request.
security: [{bearerAuth: []}]
responses:
'200':
description: Logged out.
content:
application/json:
schema:
$ref: '#/components/schemas/LogoutResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalError'
/auth/login:
post:
operationId: login
x-module: password
x-stability-level: alpha
tags: [auth]
summary: Log in with a password
description: Checks the username and password and returns a new grant. Unknown user and wrong password fail the same way.
security: []
requestBody:
description: The credentials and a description of the client.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CredentialsRequest'
responses:
'200':
description: The new grant.
headers:
Cache-Control:
$ref: '#/components/headers/CacheControlNoStore'
content:
application/json:
schema:
$ref: '#/components/schemas/GrantCreated'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'413':
$ref: '#/components/responses/PayloadTooLarge'
'429':
$ref: '#/components/responses/TooManyRequests'
'500':
$ref: '#/components/responses/InternalError'
/auth/setup:
post:
operationId: setupFirstAdmin
x-module: password
x-stability-level: alpha
tags: [auth]
summary: Create the first admin
description: "Creates the first administrator while `setupRequired` is true and returns a grant for it. Answers 409 `setup_complete` once any user exists. A server with no setup step always answers 409."
security: []
requestBody:
description: The credentials and a description of the client.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CredentialsRequest'
responses:
'201':
description: The admin was created.
headers:
Cache-Control:
$ref: '#/components/headers/CacheControlNoStore'
content:
application/json:
schema:
$ref: '#/components/schemas/GrantCreated'
'400':
$ref: '#/components/responses/BadRequest'
'409':
$ref: '#/components/responses/Conflict'
'413':
$ref: '#/components/responses/PayloadTooLarge'
'429':
$ref: '#/components/responses/TooManyRequests'
'500':
$ref: '#/components/responses/InternalError'
/auth/password:
post:
operationId: changePassword
x-module: password
x-scope: password
x-stability-level: alpha
tags: [auth]
summary: Change my password
description: "Changes the caller's password. By default every other grant of the user is revoked; the calling grant survives. On Navidrome the change also ends the user's sessions on its other APIs, regardless of `revokeOtherGrants`, which only covers API v1 grants. Answers 409 `password_managed_externally` when the password is not stored by this server."
security: [{bearerAuth: []}]
requestBody:
description: The current and the new password.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PasswordChangeRequest'
responses:
'204':
description: Password changed.
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'409':
$ref: '#/components/responses/Conflict'
'413':
$ref: '#/components/responses/PayloadTooLarge'
'429':
$ref: '#/components/responses/TooManyRequests'
'500':
$ref: '#/components/responses/InternalError'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: "Grant secret from a login method (`POST /auth/login`, `POST /auth/setup`). Opaque. The required scope is in each operation's `x-scope`."
schemas:
ServerInfo:
type: object
description: Public server description. Everything an add-server screen needs before login.
required:
- name
- serverVersion
- specVersion
- setupRequired
- loginMethods
properties:
name:
type: string
description: Human-readable server product name.
serverVersion:
type: string
description: Version of the running server build.
specVersion:
type: string
description: Version of the OpenAPI document this server implements.
setupRequired:
type: boolean
description: True until the first admin user has been created.
loginMethods:
$ref: '#/components/schemas/LoginMethods'
LoginMethods:
type: object
description: |
Login methods this server accepts, keyed by method. A missing key means the method is not offered.
Keys are optional on purpose: discovery is read by clients of any version against servers of any
version, so new methods are added as new optional keys. Clients ignore keys they do not know.
properties:
password:
$ref: '#/components/schemas/PasswordLoginMethod'
PasswordLoginMethod:
type: object
description: Username and password login (`POST /auth/login`). No settings yet.
Problem:
type: object
description: RFC 9457 problem details, returned for every 4xx and 5xx response.
required:
- title
- status
- code
properties:
type:
type: string
description: |
URI reference identifying the problem type. Omitted while the problem carries no semantics
beyond its HTTP status code, which RFC 9457 defines as `about:blank`. Problems with their
own semantics get their own URI; switch on `code` instead.
title:
type: string
description: Short human-readable summary, the same for all occurrences of this problem type.
status:
type: integer
description: HTTP status code of this response.
detail:
type: string
description: Human-readable explanation specific to this occurrence. Omitted unless the server marked the text as safe to show clients.
code:
type: string
description: Machine-readable error code, and the value clients switch on. New codes may be added.
enum:
- validation
- unauthorized
- forbidden
- insufficient_scope
- not_found
- method_not_allowed
- setup_complete
- password_managed_externally
- payload_too_large
- rate_limited
- unavailable
- internal
referenceId:
type: string
description: Present on internal errors. Quote it when reporting a problem; it tags the server's log lines for this request.
errors:
type: array
description: Per-field failures. Present only when `code` is `validation`.
items:
$ref: '#/components/schemas/ValidationError'
ValidationError:
type: object
description: One field-level validation failure.
required:
- field
- message
properties:
field:
type: string
description: Name of the offending query parameter, path parameter, or body field (dotted for nested).
message:
type: string
description: Why the value was rejected.
Capabilities:
type: object
description: |
Capability modules this server implements, keyed by module. Keys are optional; a missing key means the
module is not implemented. New modules are added as new optional keys. These are server facts, not what
the calling grant may use.
properties:
core:
$ref: '#/components/schemas/CoreCapability'
password:
$ref: '#/components/schemas/PasswordCapability'
CoreCapability:
type: object
description: The mandatory core module.
required:
- version
properties:
version:
type: integer
description: Module version. Bumped only on semantic change.
PasswordCapability:
type: object
description: The password login module (login, first-admin setup, password change).
required:
- version
properties:
version:
type: integer
description: Module version. Bumped only on semantic change.
GrantList:
type: object
description: "A page of the caller's grants."
required:
- items
- total
- offset
- limit
properties:
items:
type: array
description: "Grants on this page, by last use, most recent first; never-used grants last."
items:
$ref: '#/components/schemas/Grant'
total:
type: integer
description: Total number of grants.
offset:
type: integer
description: Zero-based index of the first returned item.
limit:
type: integer
description: Maximum number of items in this page.
LogoutResponse:
type: object
description: Result of a logout.
required:
- logoutUrl
properties:
logoutUrl:
type: string
nullable: true
description: "Where to send the browser to finish logging out of an external provider. Null when there is nothing more to do."
CredentialsRequest:
type: object
description: "Username, password and client description for a login or first-admin setup."
required:
- username
- password
- client
properties:
username:
type: string
minLength: 1
maxLength: 255
description: Login name.
password:
type: string
minLength: 1
maxLength: 1024
description: Password.
client:
type: string
minLength: 1
maxLength: 64
description: Name of the client app.
clientVersion:
type: string
maxLength: 32
description: Version of the client app.
name:
type: string
minLength: 1
maxLength: 64
description: "Label for this grant. Defaults to `client`."
scopes:
type: array
maxItems: 32
description: "Scopes the grant may hold. Omit for `all`."
items:
$ref: '#/components/schemas/ScopeRequest'
GrantCreated:
type: object
description: "Returned by every login method. The secret is shown only here; store it and never parse it."
required:
- secret
- grant
- user
properties:
secret:
type: string
maxLength: 512
description: "Opaque grant secret. Send it as `Authorization: Bearer <secret>`."
grant:
description: The new grant.
allOf:
- $ref: '#/components/schemas/Grant'
user:
description: The user the grant belongs to.
allOf:
- $ref: '#/components/schemas/AuthUser'
PasswordChangeRequest:
type: object
description: "Change the caller's own password."
required:
- currentPassword
- newPassword
properties:
currentPassword:
type: string
minLength: 1
maxLength: 1024
description: The current password.
newPassword:
type: string
minLength: 1
maxLength: 1024
description: The new password.
revokeOtherGrants:
type: boolean
default: true
description: "Revoke every other grant of the user. The calling grant always survives. Default true."
Grant:
type: object
description: A long-lived grant held by one client of one user.
required:
- id
- name
- client
- clientVersion
- scopes
- provider
- createdAt
- lastUsedAt
- lastUsedIp
- current
properties:
id:
type: string
description: Grant id.
name:
type: string
description: Label shown to the user.
client:
type: string
description: Name of the client app that holds the grant.
clientVersion:
type: string
nullable: true
description: "Version of the client app, when it sent one."
scopes:
type: array
description: Scopes this grant carries.
items:
$ref: '#/components/schemas/Scope'
provider:
type: string
description: "How the grant was created, for example `password` or `setup`. Free-form; new values may appear."
createdAt:
type: string
format: date-time
description: When the grant was created.
lastUsedAt:
type: string
format: date-time
nullable: true
description: "When the grant was last used, at a coarse granularity. Null until first use."
lastUsedIp:
type: string
nullable: true
description: Client IP of the last use. Null until first use.
current:
type: boolean
description: True for the grant that made this request.
Scope:
type: string
description: |
A permission scope. Scopes mirror capability modules; `x:write` includes `x`. `all` appears only on
grants and means every scope the user is entitled to, now and in future releases. New scopes may be added.
enum:
- all
- read
- password
ScopeRequest:
type: string
description: "A requested scope. Scopes the server does not know are dropped, not rejected, so newer clients keep working."
pattern: '^[a-z][a-z-]*(:write)?$'
maxLength: 64
AuthUser:
type: object
description: The user a grant belongs to.
required:
- id
- userName
- name
- isAdmin
- passwordChangeable
properties:
id:
type: string
description: User id.
userName:
type: string
description: Login name.
name:
type: string
description: Display name.
isAdmin:
type: boolean
description: Whether the user is an administrator.
passwordChangeable:
type: boolean
description: "Whether `POST /auth/password` can change this user's password. Clients hide \"change password\" when false."
responses:
InternalError:
description: Unexpected server failure. Details are in the server log.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
Unauthorized:
description: Missing, invalid, or expired credentials.
headers:
WWW-Authenticate:
$ref: '#/components/headers/WWWAuthenticate'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
NotModified:
description: Not modified.
headers:
ETag:
$ref: '#/components/headers/ETag'
BadRequest:
description: The request is malformed or fails validation.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
Forbidden:
description: The caller is authenticated but not allowed to do this.
headers:
WWW-Authenticate:
$ref: '#/components/headers/WWWAuthenticate'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
NotFound:
description: No such resource or endpoint.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
PayloadTooLarge:
description: "The request body is too large (`payload_too_large`)."
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
TooManyRequests:
description: "Rate limited (`rate_limited`). Retry after the `Retry-After` seconds."
headers:
Retry-After:
description: Seconds to wait before retrying.
schema:
type: integer
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
Conflict:
description: "The request conflicts with the server's state, for example `setup_complete` or `password_managed_externally`."
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
parameters:
offset:
name: offset
in: query
description: Zero-based index of the first item to return.
required: false
schema:
type: integer
minimum: 0
default: 0
limit:
name: limit
in: query
description: Maximum number of items to return.
required: false
schema:
type: integer
minimum: 1
maximum: 2000
default: 100
headers:
WWWAuthenticate:
description: 'RFC 6750 Bearer challenge, for example `Bearer error="insufficient_scope", scope="read"`.'
schema:
type: string
ETag:
description: Entity tag for `If-None-Match` revalidation.
schema:
type: string
CacheControlNoStore:
description: Always `no-store`, because the response carries a secret.
schema:
type: string
enum:
- no-store