mirror of
https://github.com/suitenumerique/meet.git
synced 2026-10-23 04:55:36 +00:00
d65ac8c845
The application token endpoint implements the client credentials
grant (RFC 6749 section 4.4), but its errors did not follow section
5.2: serializer errors were keyed by field, and the other errors used
either `detail` or a free-text `error`. Standard OAuth 2.0 client
libraries could not parse them.
Every error is now rendered as
`{"error": <code>, "error_description": <text>}` by a view-specific
exception handler:
- missing or blank client credentials and wrong credentials or
inactive applications return `invalid_client`
- an unsupported grant type returns `unsupported_grant_type`
- an invalid email, a denied domain or an unknown user return
`invalid_scope`;
- the provisional user creation conflict returns `server_error`,
since the RFC defines no code for server-side failures here
- any other error returns `invalid_request`.
Serializer errors are flattened into one description listing every
invalid field. Descriptions are restricted to the characters allowed
by the RFC, as DRF messages may be translated or contain quotes.
700 lines
24 KiB
YAML
700 lines
24 KiB
YAML
openapi: 3.0.3
|
||
info:
|
||
title: Meet External API
|
||
version: 1.0.0
|
||
description: |
|
||
External API for room management with application-delegated authentication.
|
||
|
||
#### Authentication Flow
|
||
|
||
1. Exchange application credentials for a JWT token via `/external-api/v1.0/application/token/`.
|
||
2. Use the JWT token in the `Authorization: Bearer <token>` header for all subsequent requests.
|
||
3. Tokens are scoped and allow applications to act on behalf of specific users.
|
||
|
||
#### Scopes
|
||
|
||
* `rooms:list` – List rooms accessible to the delegated user.
|
||
* `rooms:retrieve` – Retrieve details of a specific room.
|
||
* `rooms:create` – Create new rooms.
|
||
* `rooms:update` – Update the access level and configuration of existing rooms.
|
||
* `rooms:delete` – **Coming soon** Delete rooms generated by the application.
|
||
|
||
#### Upcoming Features
|
||
|
||
* **Add attendees to a room:** You will be able to update a room to include a list of attendees, allowing them to bypass the lobby system automatically.
|
||
* **Delete application-generated rooms:** Rooms created via the application can be deleted when no longer needed.
|
||
|
||
contact:
|
||
name: API Support
|
||
email: antoine.lebaud@mail.numerique.gouv.fr
|
||
|
||
servers:
|
||
- url: https://visio-sandbox.beta.numerique.gouv.fr/external-api/v1.0
|
||
description: Sandbox server
|
||
|
||
tags:
|
||
- name: Authentication
|
||
description: Application authentication and token generation
|
||
- name: Rooms
|
||
description: Room management operations
|
||
|
||
paths:
|
||
/application/token/:
|
||
post:
|
||
tags:
|
||
- Authentication
|
||
summary: Generate JWT token
|
||
description: |
|
||
Exchange application credentials for a scoped JWT token that allows the application
|
||
to act on behalf of a specific user.
|
||
|
||
The application must be authorized for the user's email domain.
|
||
The returned token expires after a configured duration and must be refreshed by calling this endpoint again.
|
||
|
||
Request parameters may be sent either as "application/x-www-form-urlencoded"
|
||
(as specified by RFC 6749 for OAuth 2.0 token endpoints) or as "application/json".
|
||
operationId: generateToken
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/x-www-form-urlencoded:
|
||
schema:
|
||
$ref: '#/components/schemas/TokenRequest'
|
||
examples:
|
||
tokenRequest:
|
||
summary: Request token for user delegation
|
||
value:
|
||
client_id: "550e8400-e29b-41d4-a716-446655440000"
|
||
client_secret: "1234567890abcdefghijklmnopqrstuvwxyz"
|
||
grant_type: "client_credentials"
|
||
scope: "user@example.com"
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/TokenRequest'
|
||
examples:
|
||
tokenRequest:
|
||
summary: Request token for user delegation
|
||
value:
|
||
client_id: "550e8400-e29b-41d4-a716-446655440000"
|
||
client_secret: "1234567890abcdefghijklmnopqrstuvwxyz"
|
||
grant_type: "client_credentials"
|
||
scope: "user@example.com"
|
||
responses:
|
||
'200':
|
||
description: Token generated successfully
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/TokenResponse'
|
||
examples:
|
||
tokenResponse:
|
||
summary: Successful token generation
|
||
value:
|
||
access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||
token_type: "Bearer"
|
||
expires_in: 3600
|
||
scope: "rooms:list rooms:retrieve rooms:create"
|
||
'400':
|
||
description: |
|
||
Invalid request. When several parameters are invalid, `error` reports
|
||
the most relevant one (client credentials first, then grant type) and
|
||
`error_description` lists every invalid parameter.
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/OAuthError'
|
||
examples:
|
||
missingClientCredentials:
|
||
summary: Missing client credentials
|
||
value:
|
||
error: "invalid_client"
|
||
error_description: "client_id : This field is required.; client_secret : This field is required."
|
||
unsupportedGrantType:
|
||
summary: Unsupported grant type
|
||
value:
|
||
error: "unsupported_grant_type"
|
||
error_description: "grant_type : 'authorization_code' is not a valid choice."
|
||
missingParameter:
|
||
summary: Missing required parameter
|
||
value:
|
||
error: "invalid_request"
|
||
error_description: "grant_type : This field is required."
|
||
invalidScope:
|
||
summary: Scope is not a valid email address
|
||
value:
|
||
error: "invalid_scope"
|
||
error_description: "Scope should be a valid email address."
|
||
'401':
|
||
description: Client authentication failed
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/OAuthError'
|
||
examples:
|
||
invalidCredentials:
|
||
summary: Invalid credentials
|
||
value:
|
||
error: "invalid_client"
|
||
error_description: "Invalid credentials"
|
||
inactiveApplication:
|
||
summary: Application is inactive
|
||
value:
|
||
error: "invalid_client"
|
||
error_description: "Application is inactive"
|
||
'403':
|
||
description: Access denied - cannot delegate user
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/OAuthError'
|
||
examples:
|
||
delegationDenied:
|
||
summary: Domain not authorized
|
||
value:
|
||
error: "invalid_scope"
|
||
error_description: "This application is not authorized for this email domain."
|
||
'404':
|
||
description: |
|
||
User not found. Only returned when the application is not allowed
|
||
to create users.
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/OAuthError'
|
||
examples:
|
||
userNotFound:
|
||
summary: User not found
|
||
value:
|
||
error: "invalid_scope"
|
||
error_description: "User not found."
|
||
'409':
|
||
description: |
|
||
The user could not be created or retrieved because of a concurrent
|
||
request. Retrying the request should succeed.
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/OAuthError'
|
||
examples:
|
||
provisionalUserConflict:
|
||
summary: Concurrent user creation
|
||
value:
|
||
error: "server_error"
|
||
error_description: "Failed to create or retrieve provisional user."
|
||
'415':
|
||
description: |
|
||
Unsupported media type. The request body must be sent as
|
||
"application/x-www-form-urlencoded" or "application/json".
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/OAuthError'
|
||
examples:
|
||
unsupportedMediaType:
|
||
summary: Unsupported request content type
|
||
value:
|
||
error: "invalid_request"
|
||
error_description: "Unsupported media type 'text/plain' in request."
|
||
|
||
/rooms:
|
||
get:
|
||
tags:
|
||
- Rooms
|
||
summary: List rooms
|
||
description: |
|
||
Returns a list of rooms accessible to the authenticated user.
|
||
Only rooms where the delegated user has access will be returned.
|
||
operationId: listRooms
|
||
security:
|
||
- BearerAuth: [rooms:list]
|
||
parameters:
|
||
- name: page
|
||
in: query
|
||
description: Page number for pagination
|
||
schema:
|
||
type: integer
|
||
minimum: 1
|
||
default: 1
|
||
- name: page_size
|
||
in: query
|
||
description: Number of items per page
|
||
schema:
|
||
type: integer
|
||
minimum: 1
|
||
maximum: 100
|
||
default: 20
|
||
responses:
|
||
'200':
|
||
description: List of accessible rooms
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
properties:
|
||
count:
|
||
type: integer
|
||
description: Total number of rooms
|
||
next:
|
||
type: string
|
||
nullable: true
|
||
description: URL to next page
|
||
previous:
|
||
type: string
|
||
nullable: true
|
||
description: URL to previous page
|
||
results:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/Room'
|
||
examples:
|
||
roomList:
|
||
summary: Paginated room list
|
||
value:
|
||
count: 2
|
||
next: "https://visio-sandbox.beta.numerique.gouv.fr/external-api/v1.0/rooms?page=2"
|
||
previous: null
|
||
results:
|
||
- id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
|
||
slug: "aae-erez-aaz"
|
||
access_level: "trusted"
|
||
url: "https://visio-sandbox.beta.numerique.gouv.fr/aae-erez-aaz"
|
||
telephony:
|
||
enabled: true
|
||
pin_code: "123456"
|
||
phone_number: "+1-555-0100"
|
||
default_country: "US"
|
||
configuration: {}
|
||
'401':
|
||
$ref: '#/components/responses/UnauthorizedError'
|
||
'403':
|
||
$ref: '#/components/responses/ForbiddenError'
|
||
|
||
/rooms/:
|
||
post:
|
||
tags:
|
||
- Rooms
|
||
summary: Create a room
|
||
description: |
|
||
Creates a new room with secure defaults for external API usage.
|
||
|
||
**Defaults:**
|
||
- Delegated user is set as owner
|
||
- Room slug auto-generated for uniqueness
|
||
- Telephony PIN auto-generated when enabled
|
||
- Creation tracked with application client_id for auditing
|
||
operationId: createRoom
|
||
security:
|
||
- BearerAuth: [rooms:create]
|
||
requestBody:
|
||
required: false
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/RoomCreate'
|
||
examples:
|
||
emptyBody:
|
||
summary: No parameters (use all defaults)
|
||
value: { }
|
||
withAccessLevel:
|
||
summary: Specify access level
|
||
value:
|
||
access_level: "trusted"
|
||
withConfiguration:
|
||
summary: Provide room configuration
|
||
value:
|
||
configuration:
|
||
everyone_can_mute: true
|
||
responses:
|
||
'201':
|
||
description: Room created successfully
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Room'
|
||
'401':
|
||
$ref: '#/components/responses/UnauthorizedError'
|
||
'403':
|
||
$ref: '#/components/responses/ForbiddenError'
|
||
|
||
/rooms/{id}:
|
||
get:
|
||
tags:
|
||
- Rooms
|
||
summary: Retrieve a room
|
||
description: Get detailed information about a specific room by its ID
|
||
operationId: retrieveRoom
|
||
security:
|
||
- BearerAuth: [rooms:retrieve]
|
||
parameters:
|
||
- name: id
|
||
in: path
|
||
required: true
|
||
description: Room UUID
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
responses:
|
||
'200':
|
||
description: Room details
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Room'
|
||
examples:
|
||
room:
|
||
summary: Room details
|
||
value:
|
||
id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
|
||
slug: "aae-erez-aaz"
|
||
access_level: "trusted"
|
||
url: "https://visio-sandbox.beta.numerique.gouv.fr/aae-erez-aaz"
|
||
telephony:
|
||
enabled: true
|
||
pin_code: "123456"
|
||
phone_number: "+1-555-0100"
|
||
default_country: "US"
|
||
configuration: {}
|
||
'401':
|
||
$ref: '#/components/responses/UnauthorizedError'
|
||
'403':
|
||
$ref: '#/components/responses/ForbiddenError'
|
||
'404':
|
||
$ref: '#/components/responses/RoomNotFoundError'
|
||
|
||
patch:
|
||
tags:
|
||
- Rooms
|
||
summary: Update a room
|
||
description: |
|
||
Partially updates a room. Only the delegated user's rooms where they are
|
||
administrator or owner can be updated; any other role gets a `403`.
|
||
|
||
**Updatable fields:** `access_level` and `configuration`. Every other field
|
||
(`id`, `name`, `slug`, `pin_code`) is read-only and silently ignored when sent.
|
||
|
||
`configuration` is replaced as a whole, it is not merged with the stored one.
|
||
Send the complete object you want the room to end up with.
|
||
|
||
Full replacement (`PUT`) is not supported. Use `PATCH` instead.
|
||
operationId: updateRoom
|
||
security:
|
||
- BearerAuth: [rooms:update]
|
||
parameters:
|
||
- name: id
|
||
in: path
|
||
required: true
|
||
description: Room UUID
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/RoomUpdate'
|
||
examples:
|
||
accessLevelOnly:
|
||
summary: Change the access level
|
||
value:
|
||
access_level: "restricted"
|
||
configurationOnly:
|
||
summary: Replace the room configuration
|
||
value:
|
||
configuration:
|
||
everyone_can_mute: true
|
||
responses:
|
||
'200':
|
||
description: Room updated successfully
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Room'
|
||
'400':
|
||
$ref: '#/components/responses/BadRequestError'
|
||
'401':
|
||
$ref: '#/components/responses/UnauthorizedError'
|
||
'403':
|
||
$ref: '#/components/responses/ForbiddenError'
|
||
'404':
|
||
$ref: '#/components/responses/RoomNotFoundError'
|
||
'405':
|
||
description: |
|
||
Method not allowed, `PUT` is not supported on this endpoint.
|
||
|
||
components:
|
||
securitySchemes:
|
||
BearerAuth:
|
||
type: http
|
||
scheme: bearer
|
||
bearerFormat: JWT
|
||
description: |
|
||
JWT token obtained from the `/application/token/` endpoint.
|
||
Include in requests as: `Authorization: Bearer <token>`
|
||
|
||
schemas:
|
||
TokenRequest:
|
||
type: object
|
||
required:
|
||
- client_id
|
||
- client_secret
|
||
- grant_type
|
||
- scope
|
||
properties:
|
||
client_id:
|
||
type: string
|
||
description: Application client identifier
|
||
example: "550e8400-e29b-41d4-a716-446655440000"
|
||
client_secret:
|
||
type: string
|
||
format: password
|
||
writeOnly: true
|
||
description: Application secret key
|
||
example: "1234567890abcdefghijklmnopqrstuvwxyz"
|
||
grant_type:
|
||
type: string
|
||
enum:
|
||
- client_credentials
|
||
description: OAuth2 grant type (must be 'client_credentials')
|
||
example: "client_credentials"
|
||
scope:
|
||
type: string
|
||
format: email
|
||
description: |
|
||
Email address of the user to delegate.
|
||
The application will act on behalf of this user.
|
||
Note: This parameter is named 'scope' to align with OAuth2 conventions,
|
||
but accepts an email address to identify the user. This design allows
|
||
for future extensibility.
|
||
example: "user@example.com"
|
||
|
||
TokenResponse:
|
||
type: object
|
||
properties:
|
||
access_token:
|
||
type: string
|
||
description: JWT access token
|
||
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtZWV0LWFwaSIsImF1ZCI6Im1lZXQtY2xpZW50cyIsImlhdCI6MTcwOTQ5MTIwMCwiZXhwIjoxNzA5NDk0ODAwLCJjbGllbnRfaWQiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAiLCJzY29wZSI6InJvb21zOmxpc3Qgcm9vbXM6cmV0cmlldmUgcm9vbXM6Y3JlYXRlIiwidXNlcl9pZCI6IjdiOGQ5YzQwLTNhMmItNGVkZi04NzFjLTJmM2Q0ZTVmNmE3YiIsImRlbGVnYXRlZCI6dHJ1ZX0.signature"
|
||
token_type:
|
||
type: string
|
||
description: Token type (always 'Bearer')
|
||
example: "Bearer"
|
||
expires_in:
|
||
type: integer
|
||
description: Token lifetime in seconds
|
||
example: 3600
|
||
scope:
|
||
type: string
|
||
description: Space-separated list of granted permission scopes
|
||
example: "rooms:list rooms:retrieve rooms:create"
|
||
|
||
RoomCreate:
|
||
type: object
|
||
description: |
|
||
Optional fields for room creation. All fields have secure defaults if omitted.
|
||
properties:
|
||
access_level:
|
||
$ref: '#/components/schemas/RoomAccessLevel'
|
||
configuration:
|
||
$ref: '#/components/schemas/RoomConfiguration'
|
||
|
||
RoomUpdate:
|
||
type: object
|
||
description: |
|
||
Fields that can be updated on an existing room. Both are optional, omitted
|
||
fields keep their current value.
|
||
properties:
|
||
access_level:
|
||
$ref: '#/components/schemas/RoomAccessLevel'
|
||
configuration:
|
||
$ref: '#/components/schemas/RoomConfiguration'
|
||
|
||
RoomConfiguration:
|
||
type: object
|
||
description: |
|
||
Optional room behaviour settings. Unknown fields are rejected.
|
||
All fields are optional and default to `null` (server-side defaults apply).
|
||
properties:
|
||
can_publish_sources:
|
||
type: array
|
||
nullable: true
|
||
description: |
|
||
Restricts which media tracks participants are allowed to publish.
|
||
If `null`, all sources are permitted.
|
||
items:
|
||
type: string
|
||
enum:
|
||
- camera
|
||
- microphone
|
||
- screen_share
|
||
- screen_share_audio
|
||
example: [ "camera", "microphone" ]
|
||
everyone_can_mute:
|
||
type: boolean
|
||
nullable: true
|
||
description: |
|
||
Whether any participant can mute others, or only the room owner/moderator.
|
||
If `null`, the server default applies.
|
||
example: true
|
||
additionalProperties: false
|
||
|
||
RoomAccessLevel:
|
||
type: string
|
||
enum:
|
||
- public
|
||
- trusted
|
||
- restricted
|
||
description: |
|
||
Controls who can join the room without going through the lobby.
|
||
|
||
- `public`: Anyone with the room link can join directly, no authentication required.
|
||
- `trusted`: Authenticated users join directly. Unauthenticated users wait in the lobby for approval.
|
||
- `restricted`: Only participants explicitly trusted by the owner bypass the lobby. Everyone else waits for approval regardless of authentication.
|
||
|
||
`public` is rejected with a `400` unless the deployment explicitly enables it
|
||
for this API. This applies both when creating a room and when updating one.
|
||
example: "trusted"
|
||
|
||
Room:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
readOnly: true
|
||
description: Unique room identifier
|
||
example: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
|
||
slug:
|
||
type: string
|
||
readOnly: true
|
||
description: URL-friendly room identifier (auto-generated)
|
||
example: "aze-eere-zer"
|
||
access_level:
|
||
$ref: '#/components/schemas/RoomAccessLevel'
|
||
url:
|
||
type: string
|
||
format: uri
|
||
readOnly: true
|
||
description: Full URL to access the room
|
||
example: "https://visio-sandbox.beta.numerique.gouv.fr/aze-eere-zer"
|
||
telephony:
|
||
type: object
|
||
readOnly: true
|
||
description: Telephony dial-in information (if enabled)
|
||
properties:
|
||
enabled:
|
||
type: boolean
|
||
description: Whether telephony is available
|
||
example: true
|
||
pin_code:
|
||
type: string
|
||
description: PIN code for dial-in access
|
||
example: "123456"
|
||
phone_number:
|
||
type: string
|
||
description: Phone number to dial
|
||
example: "+1-555-0100"
|
||
default_country:
|
||
type: string
|
||
description: Default country code
|
||
example: "US"
|
||
configuration:
|
||
$ref: '#/components/schemas/RoomConfiguration'
|
||
|
||
OAuthError:
|
||
type: object
|
||
description: OAuth 2.0 error response (RFC 6749, section 5.2)
|
||
required:
|
||
- error
|
||
- error_description
|
||
properties:
|
||
error:
|
||
type: string
|
||
description: |
|
||
Error code. `server_error` is not defined by RFC 6749 for the token
|
||
endpoint and is only returned on an unexpected server-side failure.
|
||
enum:
|
||
- invalid_request
|
||
- invalid_client
|
||
- unsupported_grant_type
|
||
- invalid_scope
|
||
- server_error
|
||
example: "invalid_client"
|
||
error_description:
|
||
type: string
|
||
description: |
|
||
Human-readable description of the error. Contains printable ASCII
|
||
characters only, without double quotes or backslashes.
|
||
example: "Invalid credentials"
|
||
|
||
Error:
|
||
type: object
|
||
properties:
|
||
detail:
|
||
type: string
|
||
description: Error message
|
||
example: "Invalid token."
|
||
|
||
ValidationError:
|
||
type: object
|
||
properties:
|
||
field_name:
|
||
type: array
|
||
items:
|
||
type: string
|
||
description: List of validation errors for this field
|
||
example: ["This field is required."]
|
||
|
||
responses:
|
||
UnauthorizedError:
|
||
description: Authentication required or token invalid/expired
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
examples:
|
||
invalidToken:
|
||
summary: Invalid or expired token
|
||
value:
|
||
error: "Invalid token."
|
||
tokenExpired:
|
||
summary: Token has expired
|
||
value:
|
||
error: "Token expired."
|
||
|
||
ForbiddenError:
|
||
description: Insufficient permissions for this operation
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
examples:
|
||
insufficientScope:
|
||
summary: Missing required scope
|
||
value:
|
||
detail: "Insufficient permissions. Required scope: 'rooms:xxxx'"
|
||
|
||
RoomNotFoundError:
|
||
description: Room not found
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Error'
|
||
examples:
|
||
roomNotFound:
|
||
summary: Room does not exist
|
||
value:
|
||
detail: "Not found."
|
||
|
||
BadRequestError:
|
||
description: Invalid request data
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ValidationError'
|
||
examples:
|
||
validationError:
|
||
summary: Field validation failed
|
||
value:
|
||
scope: ["Invalid email address."]
|