navidrome/api/bundled/openapi.json
Deluan 294c1a9a6a feat(api): add API v1 login, setup, token, grant and password endpoints
Adds the seven auth operations to the spec (createAccessToken, listGrants,
revokeGrant and logout in core; login, setupFirstAdmin and changePassword in
the password module), the bearerAuth/grantAuth schemes, and vacuum rules
requiring explicit security and a known x-scope. The strict handlers sit on
core/apiauth and are covered end to end against a real SQLite database.

The first operations with parameters make the generated code import
github.com/oapi-codegen/runtime. An oapi-codegen overlay renames the shared
offset/limit parameter types, since a generated Offset clashes with Ginkgo's
dot-imported Offset in this package's tests; the published spec is unchanged.
2026-09-28 20:06:27 -04:00

1064 lines
No EOL
32 KiB
JSON

{
"openapi": "3.0.3",
"info": {
"title": "Navidrome API",
"version": "1.0.0",
"description": "Navidrome API v1. Spec-first, additive within v1. Clients discover implemented\ncapability modules through `GET /server` and never sniff versions.\n\nEnums are open: new values may be added to any enum within v1. Clients must\naccept values they do not recognise instead of failing.\n\nEvery operation declares `x-stability-level`: `alpha` operations may change or\ndisappear without notice, `beta` and `stable` operations only change additively.\nA level is only ever raised, never lowered.\n\n`HEAD` is accepted wherever `GET` is. A `405` response lists the allowed methods\nin its `Allow` header.\n\nOperations that need an access token declare `security: [{bearerAuth: []}]` and the scope they need in\n`x-scope` (OpenAPI 3.0 does not allow scopes on bearer schemes). A revoked grant, and every token minted\nfrom it, stops working within one access-token lifetime at most.\n",
"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, access tokens, 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.\nAuthenticated requests will additionally receive the implemented capability modules\nonce authentication is available.\n",
"responses": {
"200": {
"description": "Server description.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerInfo"
}
}
}
},
"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/token": {
"post": {
"operationId": "createAccessToken",
"x-module": "core",
"x-stability-level": "alpha",
"tags": [
"auth"
],
"summary": "Mint an access token",
"description": "Turns a grant into a short-lived access token, optionally narrowed to a subset of the grant's scopes. Send the grant secret as the Bearer credential.",
"security": [
{
"grantAuth": []
}
],
"requestBody": {
"description": "Scopes to narrow the token to. Optional.",
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TokenRequest"
}
}
}
},
"responses": {
"200": {
"description": "The new access token.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AccessToken"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"413": {
"$ref": "#/components/responses/PayloadTooLarge"
},
"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": {
"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"
}
}
}
},
"/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 and every token minted from it. 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.",
"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.",
"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. 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": "Short-lived access token from `POST /auth/token`. Opaque. The required scope is in each operation's `x-scope`."
},
"grantAuth": {
"type": "http",
"scheme": "bearer",
"description": "Long-lived grant secret. Accepted only by `POST /auth/token`."
}
},
"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": {
"type": "array",
"description": "Login methods this server accepts. New methods may be added; clients ignore values they do not recognise.",
"items": {
"type": "string",
"enum": [
"password"
]
}
}
}
},
"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\nbeyond its HTTP status code, which RFC 9457 defines as `about:blank`. Problems with their\nown semantics get their own URI; switch on `code` instead.\n"
},
"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",
"token_expired",
"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."
}
}
},
"TokenRequest": {
"type": "object",
"description": "Optional narrowing of a new access token.",
"properties": {
"scopes": {
"type": "array",
"maxItems": 32,
"description": "Subset of the grant's scopes. Omit for all of them; an empty list asks for none.",
"items": {
"$ref": "#/components/schemas/ScopeRequest"
}
}
}
},
"AccessToken": {
"type": "object",
"description": "A short-lived access token. Opaque; clients must not decode it.",
"required": [
"accessToken",
"tokenType",
"expiresIn",
"scopes"
],
"properties": {
"accessToken": {
"type": "string",
"description": "The token. Send it as `Authorization: Bearer \u003ctoken\u003e`."
},
"tokenType": {
"type": "string",
"enum": [
"Bearer"
],
"description": "Always `Bearer`."
},
"expiresIn": {
"type": "integer",
"description": "Seconds until the token expires."
},
"scopes": {
"type": "array",
"description": "Scopes the token actually carries, which may be fewer than requested.",
"items": {
"$ref": "#/components/schemas/Scope"
}
}
}
},
"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 a Bearer credential to `POST /auth/token`."
},
"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."
}
}
},
"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
},
"Scope": {
"type": "string",
"description": "A permission scope. Scopes mirror capability modules; `x:write` includes `x`. `all` appears only on\ngrants and means every scope the user is entitled to, now and in future releases. New scopes may be added.\n",
"enum": [
"all",
"read",
"password"
]
},
"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 may mint tokens for.",
"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."
}
}
},
"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"
}
}
}
},
"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"
}
}
}
},
"Unauthorized": {
"description": "Missing, invalid, or expired credentials.",
"headers": {
"WWW-Authenticate": {
"$ref": "#/components/headers/WWWAuthenticate"
}
},
"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"
}
}
}
},
"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"
}
}
}
},
"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": {
"ETag": {
"description": "Entity tag for `If-None-Match` revalidation.",
"schema": {
"type": "string"
}
},
"WWWAuthenticate": {
"description": "RFC 6750 Bearer challenge, for example `Bearer error=\"insufficient_scope\", scope=\"read\"`.",
"schema": {
"type": "string"
}
}
}
}
}