Add User Applications REST API
What does this MR do and why?
This Merge Request adds the User Applications REST API, fulfilling the requirements described in #23054.
It introduces a set of CRUD endpoints located at /api/v4/user/applications that allow users to manage their instance-level OAuth applications. This is designed to enable programmatic management of user-owned OAuth applications, which proves extremely useful for dynamically deploying and un-deploying Review Apps without having to employ insecure wildcard redirect URIs.
Specifically, it creates the following endpoints:
POST /api/v4/user/applications: Creates an application, exclusively returning thesecretduring this action.GET /api/v4/user/applications: Lists all applications owned by the authenticated user (supports pagination).GET /api/v4/user/applications/:id: Retrieves a specific application.PUT /api/v4/user/applications/:id: Updates an existing application (e.g. modifying thenameorscopes). Note that sensitive fields likeredirect_uriandconfidentialcannot be mutated after creation.DELETE /api/v4/user/applications/:id: Removes the application.
Security Considerations
The implementation follows strict security best practices regarding credential exposure and bounded context:
- Secret Exclusivity: In accordance with OAuth 2.0 best practices, the application
secretis explicitly and exclusively returned only on thePOSTcreate event. SubsequentGETorPUTrequests will never expose thesecret. - Targeting Wildcards: The primary motivation for this API is to give developers a programmatic way to provision dynamic
redirect_uritargets (such as ephemeral Review Apps). This eliminates the insecure practice of users resorting to wildcard redirect URIs to handle dynamic domains. - Immutability of Sensitive Fields: To prevent redirect hijacking or unintended security downgrade chaining via API abuse, the
redirect_uriandconfidentialflags are entirely immutable through the APIPUTendpoint after initial creation. - Authorization Enforcement: The endpoint exclusively operates within the strict boundary of the
current_user. TheAuthn::UserApplicationsFinderinherently scopes allGET,PUT, andDELETErequests to only OAuth Applications owned by the authenticated user, completely preventing Insecure Direct Object Reference (IDOR) or cross-user data leakage. - Confidentiality Awareness: The endpoints explicitly support defining whether an OAuth application is confidential or public (e.g. for SPAs or Native apps), ensuring the system handles the client appropriately during the OAuth flow.
Database Queries
The User Applications API executes standard CRUD operations on the oauth_applications table, scoped to the current user. Here are the query execution plans:
1. Read collection (GET index with Pagination)
EXPLAIN SELECT "oauth_applications".* FROM "oauth_applications" WHERE "oauth_applications"."owner_id" = 1 AND "oauth_applications"."owner_type" = 'User' LIMIT 20 OFFSET 0; Limit (cost=0.15..2.17 rows=1 width=237)
-> Index Scan using index_oauth_applications_on_owner_id_and_owner_type on oauth_applications (cost=0.15..2.17 rows=1 width=237)
Index Cond: ((owner_id = 1) AND ((owner_type)::text = 'User'::text))2. Read single item (GET :id)
EXPLAIN SELECT "oauth_applications".* FROM "oauth_applications" WHERE "oauth_applications"."id" = 1 LIMIT 1; Limit (cost=0.15..2.17 rows=1 width=237)
-> Index Scan using oauth_applications_pkey on oauth_applications (cost=0.15..2.17 rows=1 width=237)
Index Cond: (id = 1)3. Create (POST)
EXPLAIN INSERT INTO "oauth_applications" ("name", "uid", "secret", "redirect_uri", "scopes", "created_at", "updated_at", "owner_id", "owner_type", "confidential", "organization_id") VALUES ('test', 'uid', 'secret', 'url', 'api', NOW(), NOW(), 1, 'User', true, 1) RETURNING "id"; Insert on oauth_applications (cost=0.00..0.02 rows=1 width=237)
-> Result (cost=0.00..0.02 rows=1 width=237)4. Update (PUT)
EXPLAIN UPDATE "oauth_applications" SET "name" = 'test2', "updated_at" = NOW() WHERE "oauth_applications"."id" = 1; Update on oauth_applications (cost=0.15..2.17 rows=0 width=0)
-> Index Scan using oauth_applications_pkey on oauth_applications (cost=0.15..2.17 rows=1 width=46)
Index Cond: (id = 1)5. Delete (DELETE)
EXPLAIN DELETE FROM "oauth_applications" WHERE "oauth_applications"."id" = 1; Delete on oauth_applications (cost=0.15..2.17 rows=0 width=0)
-> Index Scan using oauth_applications_pkey on oauth_applications (cost=0.15..2.17 rows=1 width=6)
Index Cond: (id = 1)6. Count (Pagination meta)
EXPLAIN SELECT COUNT(*) FROM "oauth_applications" WHERE "oauth_applications"."owner_id" = 1 AND "oauth_applications"."owner_type" = 'User'; Aggregate (cost=2.17..2.18 rows=1 width=8)
-> Index Only Scan using index_oauth_applications_on_owner_id_and_owner_type on oauth_applications (cost=0.15..2.17 rows=1 width=0)
Index Cond: ((owner_id = 1) AND (owner_type = 'User'::text))References
- Resolves #23054
Screenshots or screen recordings
The API returns standard GitLab API JSON shapes, and correctly provisions the underlying Authn::OauthApplication record associated with the user.
| Browser Dev Tools Verification |
|---|
![]() |
How to set up and validate locally
-
Ensure your GDK is up and running.
-
Open your browser developer console while authenticated as any user (e.g.
root) on the GitLab dashboard. -
Execute the following fetch command to provision an application:
const csrf = document.querySelector('meta[name="csrf-token"]').content; fetch('/api/v4/user/applications', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf }, body: JSON.stringify({ name: 'Browser UI Test App', redirect_uri: 'http://localhost/callback', scopes: 'api', confidential: false }) }).then(r => r.json()).then(console.log); -
Verify that the response returns a success with the
secretand anapplication_id. -
You can assert the creation of the Application by checking your
/-/profile/applicationsinterface.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
