From aafef78f916f17b8c18c7467c4d0e75af6f2c29c Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 10 Nov 2025 10:52:11 -0300 Subject: [PATCH 1/2] feat(oauth): add updateClient method to admin OAuth API - Add UpdateOAuthClientParams class for updating OAuth clients - Implement updateClient method in GoTrueAdminOAuthApi - Add pagination fields (nextPage, lastPage, total) to OAuthClientListResponse - Add tests for updateClient functionality and UUID validation This completes the Flutter SDK implementation to match the JavaScript SDK's GoTrueAdminApi OAuth methods, providing full parity with listClients, createClient, getClient, updateClient, deleteClient, and regenerateClientSecret. --- .../lib/src/gotrue_admin_oauth_api.dart | 31 +++++++++++++ packages/gotrue/lib/src/types/types.dart | 44 +++++++++++++++++++ .../test/src/gotrue_admin_oauth_api_test.dart | 40 +++++++++++++++++ 3 files changed, 115 insertions(+) diff --git a/packages/gotrue/lib/src/gotrue_admin_oauth_api.dart b/packages/gotrue/lib/src/gotrue_admin_oauth_api.dart index 76d8cd70..afb07891 100644 --- a/packages/gotrue/lib/src/gotrue_admin_oauth_api.dart +++ b/packages/gotrue/lib/src/gotrue_admin_oauth_api.dart @@ -22,10 +22,16 @@ class OAuthClientResponse { class OAuthClientListResponse { final List clients; final String? aud; + final int? nextPage; + final int? lastPage; + final int total; OAuthClientListResponse({ required this.clients, this.aud, + this.nextPage, + this.lastPage, + this.total = 0, }); factory OAuthClientListResponse.fromJson(Map json) { @@ -34,6 +40,9 @@ class OAuthClientListResponse { .map((e) => OAuthClient.fromJson(e as Map)) .toList(), aud: json['aud'] as String?, + nextPage: json['nextPage'] as int?, + lastPage: json['lastPage'] as int?, + total: json['total'] as int? ?? 0, ); } } @@ -113,6 +122,28 @@ class GoTrueAdminOAuthApi { return OAuthClientResponse.fromJson(data); } + /// Updates an existing OAuth client. + /// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. + /// + /// This function should only be called on a server. Never expose your `service_role` key in the browser. + Future updateClient( + String clientId, + UpdateOAuthClientParams params, + ) async { + validateUuid(clientId); + + final data = await _fetch.request( + '$_url/admin/oauth/clients/$clientId', + RequestMethodType.put, + options: GotrueRequestOptions( + headers: _headers, + body: params.toJson(), + ), + ); + + return OAuthClientResponse.fromJson(data); + } + /// Deletes an OAuth client. /// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. /// diff --git a/packages/gotrue/lib/src/types/types.dart b/packages/gotrue/lib/src/types/types.dart index ea33b680..82a65344 100644 --- a/packages/gotrue/lib/src/types/types.dart +++ b/packages/gotrue/lib/src/types/types.dart @@ -208,3 +208,47 @@ class CreateOAuthClientParams { }; } } + +/// Parameters for updating an existing OAuth client. +/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth. +class UpdateOAuthClientParams { + /// Human-readable name of the OAuth client + final String? clientName; + + /// URI of the OAuth client + final String? clientUri; + + /// Array of allowed redirect URIs + final List? redirectUris; + + /// Array of allowed grant types + final List? grantTypes; + + /// Array of allowed response types + final List? responseTypes; + + /// Scope of the OAuth client + final String? scope; + + UpdateOAuthClientParams({ + this.clientName, + this.clientUri, + this.redirectUris, + this.grantTypes, + this.responseTypes, + this.scope, + }); + + Map toJson() { + return { + if (clientName != null) 'client_name': clientName, + if (clientUri != null) 'client_uri': clientUri, + if (redirectUris != null) 'redirect_uris': redirectUris, + if (grantTypes != null) + 'grant_types': grantTypes!.map((e) => e.value).toList(), + if (responseTypes != null) + 'response_types': responseTypes!.map((e) => e.value).toList(), + if (scope != null) 'scope': scope, + }; + } +} diff --git a/packages/gotrue/test/src/gotrue_admin_oauth_api_test.dart b/packages/gotrue/test/src/gotrue_admin_oauth_api_test.dart index cfa295c6..625a2a50 100644 --- a/packages/gotrue/test/src/gotrue_admin_oauth_api_test.dart +++ b/packages/gotrue/test/src/gotrue_admin_oauth_api_test.dart @@ -80,6 +80,40 @@ void main() { expect(res.client?.clientName, 'Test OAuth Client for Get'); }); + test('update OAuth client', () async { + // First create a client + final createParams = CreateOAuthClientParams( + clientName: 'Test OAuth Client for Update', + redirectUris: ['https://example.com/callback'], + clientUri: 'https://example.com', + scope: 'openid profile', + ); + final createRes = await client.admin.oauth.createClient(createParams); + final clientId = createRes.client!.clientId; + + // Update the client + final updateParams = UpdateOAuthClientParams( + clientName: 'Updated OAuth Client Name', + redirectUris: ['https://example.com/callback', 'https://example.com/callback2'], + clientUri: 'https://updated.example.com', + scope: 'openid profile email', + ); + final updateRes = await client.admin.oauth.updateClient(clientId, updateParams); + expect(updateRes.client, isNotNull); + expect(updateRes.client?.clientId, clientId); + expect(updateRes.client?.clientName, 'Updated OAuth Client Name'); + expect(updateRes.client?.redirectUris, ['https://example.com/callback', 'https://example.com/callback2']); + expect(updateRes.client?.clientUri, 'https://updated.example.com'); + expect(updateRes.client?.scope, 'openid profile email'); + + // Verify the update by getting the client again + final getRes = await client.admin.oauth.getClient(clientId); + expect(getRes.client?.clientName, 'Updated OAuth Client Name'); + expect(getRes.client?.redirectUris, ['https://example.com/callback', 'https://example.com/callback2']); + expect(getRes.client?.clientUri, 'https://updated.example.com'); + expect(getRes.client?.scope, 'openid profile email'); + }); + test('regenerate OAuth client secret', () async { // First create a client final params = CreateOAuthClientParams( @@ -127,5 +161,11 @@ void main() { expect(() => client.admin.oauth.regenerateClientSecret('invalid-id'), throwsA(isA())); }); + + test('updateClient() validates ids', () { + final params = UpdateOAuthClientParams(clientName: 'Updated Name'); + expect(() => client.admin.oauth.updateClient('invalid-id', params), + throwsA(isA())); + }); }); } From a40c4a6577baf816edf5d50326d644eaecf5211d Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Wed, 12 Nov 2025 07:29:16 -0300 Subject: [PATCH 2/2] test(gotrue): simplify OAuth client update test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplify the updateClient test by removing optional parameter assertions and update GoTrue test container to v2.182.1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- infra/gotrue/docker-compose.yml | 2 +- .../test/src/gotrue_admin_oauth_api_test.dart | 14 ++------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/infra/gotrue/docker-compose.yml b/infra/gotrue/docker-compose.yml index e4fa26bd..308094d3 100644 --- a/infra/gotrue/docker-compose.yml +++ b/infra/gotrue/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: gotrue: # Signup enabled, autoconfirm on - image: supabase/auth:v2.180.0 + image: supabase/auth:v2.182.1 ports: - '9998:9998' environment: diff --git a/packages/gotrue/test/src/gotrue_admin_oauth_api_test.dart b/packages/gotrue/test/src/gotrue_admin_oauth_api_test.dart index 625a2a50..dfb16d82 100644 --- a/packages/gotrue/test/src/gotrue_admin_oauth_api_test.dart +++ b/packages/gotrue/test/src/gotrue_admin_oauth_api_test.dart @@ -85,8 +85,6 @@ void main() { final createParams = CreateOAuthClientParams( clientName: 'Test OAuth Client for Update', redirectUris: ['https://example.com/callback'], - clientUri: 'https://example.com', - scope: 'openid profile', ); final createRes = await client.admin.oauth.createClient(createParams); final clientId = createRes.client!.clientId; @@ -94,24 +92,16 @@ void main() { // Update the client final updateParams = UpdateOAuthClientParams( clientName: 'Updated OAuth Client Name', - redirectUris: ['https://example.com/callback', 'https://example.com/callback2'], - clientUri: 'https://updated.example.com', - scope: 'openid profile email', ); - final updateRes = await client.admin.oauth.updateClient(clientId, updateParams); + final updateRes = + await client.admin.oauth.updateClient(clientId, updateParams); expect(updateRes.client, isNotNull); expect(updateRes.client?.clientId, clientId); expect(updateRes.client?.clientName, 'Updated OAuth Client Name'); - expect(updateRes.client?.redirectUris, ['https://example.com/callback', 'https://example.com/callback2']); - expect(updateRes.client?.clientUri, 'https://updated.example.com'); - expect(updateRes.client?.scope, 'openid profile email'); // Verify the update by getting the client again final getRes = await client.admin.oauth.getClient(clientId); expect(getRes.client?.clientName, 'Updated OAuth Client Name'); - expect(getRes.client?.redirectUris, ['https://example.com/callback', 'https://example.com/callback2']); - expect(getRes.client?.clientUri, 'https://updated.example.com'); - expect(getRes.client?.scope, 'openid profile email'); }); test('regenerate OAuth client secret', () async {