Skip to content

Commit ebc4c20

Browse files
grdsdevclaude
andauthored
feat(gotrue): add OAuth 2.1 client admin endpoints (#1244)
* feat(gotrue): add OAuth 2.1 client admin endpoints Add support for OAuth 2.1 client administration endpoints in the gotrue package. This feature allows server-side management of OAuth clients through the admin API. New functionality: - admin.oauth.listClients(): List OAuth clients with pagination - admin.oauth.createClient(): Register new OAuth client - admin.oauth.getClient(): Get client details by ID - admin.oauth.deleteClient(): Remove OAuth client - admin.oauth.regenerateClientSecret(): Regenerate client secret Only relevant when OAuth 2.1 server is enabled in Supabase Auth. All methods require service_role key and should only be called server-side. Ported from: supabase/supabase-js#1582 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * test(gotrue): enable OAuth 2.1 server and fix response parsing - Update GoTrue from v2.175.0 to v2.180.0 - Enable OAuth server with dynamic registration in test infrastructure - Fix parsing of optional `aud` field in OAuthClientListResponse - Handle empty response bodies (204 No Content) for delete operations - Update delete test to expect null client on successful deletion All OAuth admin endpoint tests now passing, matching behavior from supabase/supabase-py#1240 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent cad1784 commit ebc4c20

File tree

6 files changed

+476
-2
lines changed

6 files changed

+476
-2
lines changed

infra/gotrue/docker-compose.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
version: '3'
33
services:
44
gotrue: # Signup enabled, autoconfirm on
5-
image: supabase/auth:v2.175.0
5+
image: supabase/auth:v2.180.0
66
ports:
77
- '9998:9998'
88
environment:
@@ -29,6 +29,8 @@ services:
2929
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: 'true'
3030
GOTRUE_MFA_PHONE_ENROLL_ENABLED: 'true'
3131
GOTRUE_MFA_PHONE_VERIFY_ENABLED: 'true'
32+
GOTRUE_OAUTH_SERVER_ENABLED: 'true'
33+
GOTRUE_OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION: 'true'
3234

3335
depends_on:
3436
- db

packages/gotrue/lib/src/fetch.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,11 @@ class GotrueFetch {
199199
}
200200

201201
try {
202-
return json.decode(utf8.decode(response.bodyBytes));
202+
final bodyString = utf8.decode(response.bodyBytes);
203+
if (bodyString.isEmpty) {
204+
return <String, dynamic>{};
205+
}
206+
return json.decode(bodyString);
203207
} catch (error) {
204208
throw _handleError(error);
205209
}

packages/gotrue/lib/src/gotrue_admin_api.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:gotrue/src/types/fetch_options.dart';
66
import 'package:http/http.dart';
77

88
import 'gotrue_admin_mfa_api.dart';
9+
import 'gotrue_admin_oauth_api.dart';
910

1011
class GoTrueAdminApi {
1112
final String _url;
@@ -15,6 +16,10 @@ class GoTrueAdminApi {
1516
late final GotrueFetch _fetch = GotrueFetch(_httpClient);
1617
late final GoTrueAdminMFAApi mfa;
1718

19+
/// Contains all OAuth client administration methods.
20+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
21+
late final GoTrueAdminOAuthApi oauth;
22+
1823
GoTrueAdminApi(
1924
this._url, {
2025
Map<String, String>? headers,
@@ -26,6 +31,11 @@ class GoTrueAdminApi {
2631
headers: _headers,
2732
fetch: _fetch,
2833
);
34+
oauth = GoTrueAdminOAuthApi(
35+
url: _url,
36+
headers: _headers,
37+
fetch: _fetch,
38+
);
2939
}
3040

3141
/// Removes a logged-in session.
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import 'fetch.dart';
2+
import 'helper.dart';
3+
import 'types/fetch_options.dart';
4+
import 'types/types.dart';
5+
6+
/// Response type for OAuth client operations.
7+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
8+
class OAuthClientResponse {
9+
final OAuthClient? client;
10+
11+
OAuthClientResponse({this.client});
12+
13+
factory OAuthClientResponse.fromJson(Map<String, dynamic> json) {
14+
return OAuthClientResponse(
15+
client: json.isEmpty ? null : OAuthClient.fromJson(json),
16+
);
17+
}
18+
}
19+
20+
/// Response type for listing OAuth clients.
21+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
22+
class OAuthClientListResponse {
23+
final List<OAuthClient> clients;
24+
final String? aud;
25+
26+
OAuthClientListResponse({
27+
required this.clients,
28+
this.aud,
29+
});
30+
31+
factory OAuthClientListResponse.fromJson(Map<String, dynamic> json) {
32+
return OAuthClientListResponse(
33+
clients: (json['clients'] as List)
34+
.map((e) => OAuthClient.fromJson(e as Map<String, dynamic>))
35+
.toList(),
36+
aud: json['aud'] as String?,
37+
);
38+
}
39+
}
40+
41+
/// Contains all OAuth client administration methods.
42+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
43+
class GoTrueAdminOAuthApi {
44+
final String _url;
45+
final Map<String, String> _headers;
46+
final GotrueFetch _fetch;
47+
48+
GoTrueAdminOAuthApi({
49+
required String url,
50+
required Map<String, String> headers,
51+
required GotrueFetch fetch,
52+
}) : _url = url,
53+
_headers = headers,
54+
_fetch = fetch;
55+
56+
/// Lists all OAuth clients with optional pagination.
57+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
58+
///
59+
/// This function should only be called on a server. Never expose your `service_role` key in the browser.
60+
Future<OAuthClientListResponse> listClients({
61+
int? page,
62+
int? perPage,
63+
}) async {
64+
final data = await _fetch.request(
65+
'$_url/admin/oauth/clients',
66+
RequestMethodType.get,
67+
options: GotrueRequestOptions(
68+
headers: _headers,
69+
query: {
70+
if (page != null) 'page': page.toString(),
71+
if (perPage != null) 'per_page': perPage.toString(),
72+
},
73+
),
74+
);
75+
76+
return OAuthClientListResponse.fromJson(data);
77+
}
78+
79+
/// Creates a new OAuth client.
80+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
81+
///
82+
/// This function should only be called on a server. Never expose your `service_role` key in the browser.
83+
Future<OAuthClientResponse> createClient(
84+
CreateOAuthClientParams params,
85+
) async {
86+
final data = await _fetch.request(
87+
'$_url/admin/oauth/clients',
88+
RequestMethodType.post,
89+
options: GotrueRequestOptions(
90+
headers: _headers,
91+
body: params.toJson(),
92+
),
93+
);
94+
95+
return OAuthClientResponse.fromJson(data);
96+
}
97+
98+
/// Gets details of a specific OAuth client.
99+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
100+
///
101+
/// This function should only be called on a server. Never expose your `service_role` key in the browser.
102+
Future<OAuthClientResponse> getClient(String clientId) async {
103+
validateUuid(clientId);
104+
105+
final data = await _fetch.request(
106+
'$_url/admin/oauth/clients/$clientId',
107+
RequestMethodType.get,
108+
options: GotrueRequestOptions(
109+
headers: _headers,
110+
),
111+
);
112+
113+
return OAuthClientResponse.fromJson(data);
114+
}
115+
116+
/// Deletes an OAuth client.
117+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
118+
///
119+
/// This function should only be called on a server. Never expose your `service_role` key in the browser.
120+
Future<OAuthClientResponse> deleteClient(String clientId) async {
121+
validateUuid(clientId);
122+
123+
final data = await _fetch.request(
124+
'$_url/admin/oauth/clients/$clientId',
125+
RequestMethodType.delete,
126+
options: GotrueRequestOptions(
127+
headers: _headers,
128+
),
129+
);
130+
131+
return OAuthClientResponse.fromJson(data);
132+
}
133+
134+
/// Regenerates the secret for an OAuth client.
135+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
136+
///
137+
/// This function should only be called on a server. Never expose your `service_role` key in the browser.
138+
Future<OAuthClientResponse> regenerateClientSecret(String clientId) async {
139+
validateUuid(clientId);
140+
141+
final data = await _fetch.request(
142+
'$_url/admin/oauth/clients/$clientId/regenerate_secret',
143+
RequestMethodType.post,
144+
options: GotrueRequestOptions(
145+
headers: _headers,
146+
),
147+
);
148+
149+
return OAuthClientResponse.fromJson(data);
150+
}
151+
}

packages/gotrue/lib/src/types/types.dart

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,179 @@ enum OAuthProvider {
3232
workos,
3333
zoom,
3434
}
35+
36+
/// OAuth client grant types supported by the OAuth 2.1 server.
37+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
38+
enum OAuthClientGrantType {
39+
authorizationCode('authorization_code'),
40+
refreshToken('refresh_token');
41+
42+
final String value;
43+
const OAuthClientGrantType(this.value);
44+
}
45+
46+
/// OAuth client response types supported by the OAuth 2.1 server.
47+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
48+
enum OAuthClientResponseType {
49+
code('code');
50+
51+
final String value;
52+
const OAuthClientResponseType(this.value);
53+
}
54+
55+
/// OAuth client type indicating whether the client can keep credentials confidential.
56+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
57+
enum OAuthClientType {
58+
public('public'),
59+
confidential('confidential');
60+
61+
final String value;
62+
const OAuthClientType(this.value);
63+
64+
static OAuthClientType fromString(String value) {
65+
return OAuthClientType.values.firstWhere((e) => e.value == value);
66+
}
67+
}
68+
69+
/// OAuth client registration type.
70+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
71+
enum OAuthClientRegistrationType {
72+
dynamic('dynamic'),
73+
manual('manual');
74+
75+
final String value;
76+
const OAuthClientRegistrationType(this.value);
77+
78+
static OAuthClientRegistrationType fromString(String value) {
79+
return OAuthClientRegistrationType.values
80+
.firstWhere((e) => e.value == value);
81+
}
82+
}
83+
84+
/// OAuth client object returned from the OAuth 2.1 server.
85+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
86+
class OAuthClient {
87+
/// Unique identifier for the OAuth client
88+
final String clientId;
89+
90+
/// Human-readable name of the OAuth client
91+
final String clientName;
92+
93+
/// Client secret (only returned on registration and regeneration)
94+
final String? clientSecret;
95+
96+
/// Type of OAuth client
97+
final OAuthClientType clientType;
98+
99+
/// Token endpoint authentication method
100+
final String tokenEndpointAuthMethod;
101+
102+
/// Registration type of the client
103+
final OAuthClientRegistrationType registrationType;
104+
105+
/// URI of the OAuth client
106+
final String? clientUri;
107+
108+
/// Array of allowed redirect URIs
109+
final List<String> redirectUris;
110+
111+
/// Array of allowed grant types
112+
final List<OAuthClientGrantType> grantTypes;
113+
114+
/// Array of allowed response types
115+
final List<OAuthClientResponseType> responseTypes;
116+
117+
/// Scope of the OAuth client
118+
final String? scope;
119+
120+
/// Timestamp when the client was created
121+
final String createdAt;
122+
123+
/// Timestamp when the client was last updated
124+
final String updatedAt;
125+
126+
OAuthClient({
127+
required this.clientId,
128+
required this.clientName,
129+
this.clientSecret,
130+
required this.clientType,
131+
required this.tokenEndpointAuthMethod,
132+
required this.registrationType,
133+
this.clientUri,
134+
required this.redirectUris,
135+
required this.grantTypes,
136+
required this.responseTypes,
137+
this.scope,
138+
required this.createdAt,
139+
required this.updatedAt,
140+
});
141+
142+
factory OAuthClient.fromJson(Map<String, dynamic> json) {
143+
return OAuthClient(
144+
clientId: json['client_id'] as String,
145+
clientName: json['client_name'] as String,
146+
clientSecret: json['client_secret'] as String?,
147+
clientType: OAuthClientType.fromString(json['client_type'] as String),
148+
tokenEndpointAuthMethod: json['token_endpoint_auth_method'] as String,
149+
registrationType: OAuthClientRegistrationType.fromString(
150+
json['registration_type'] as String),
151+
clientUri: json['client_uri'] as String?,
152+
redirectUris: (json['redirect_uris'] as List).cast<String>(),
153+
grantTypes: (json['grant_types'] as List)
154+
.map((e) => OAuthClientGrantType.values
155+
.firstWhere((gt) => gt.value == e as String))
156+
.toList(),
157+
responseTypes: (json['response_types'] as List)
158+
.map((e) => OAuthClientResponseType.values
159+
.firstWhere((rt) => rt.value == e as String))
160+
.toList(),
161+
scope: json['scope'] as String?,
162+
createdAt: json['created_at'] as String,
163+
updatedAt: json['updated_at'] as String,
164+
);
165+
}
166+
}
167+
168+
/// Parameters for creating a new OAuth client.
169+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
170+
class CreateOAuthClientParams {
171+
/// Human-readable name of the OAuth client
172+
final String clientName;
173+
174+
/// URI of the OAuth client
175+
final String? clientUri;
176+
177+
/// Array of allowed redirect URIs
178+
final List<String> redirectUris;
179+
180+
/// Array of allowed grant types (optional, defaults to authorization_code and refresh_token)
181+
final List<OAuthClientGrantType>? grantTypes;
182+
183+
/// Array of allowed response types (optional, defaults to code)
184+
final List<OAuthClientResponseType>? responseTypes;
185+
186+
/// Scope of the OAuth client
187+
final String? scope;
188+
189+
CreateOAuthClientParams({
190+
required this.clientName,
191+
this.clientUri,
192+
required this.redirectUris,
193+
this.grantTypes,
194+
this.responseTypes,
195+
this.scope,
196+
});
197+
198+
Map<String, dynamic> toJson() {
199+
return {
200+
'client_name': clientName,
201+
if (clientUri != null) 'client_uri': clientUri,
202+
'redirect_uris': redirectUris,
203+
if (grantTypes != null)
204+
'grant_types': grantTypes!.map((e) => e.value).toList(),
205+
if (responseTypes != null)
206+
'response_types': responseTypes!.map((e) => e.value).toList(),
207+
if (scope != null) 'scope': scope,
208+
};
209+
}
210+
}

0 commit comments

Comments
 (0)