Skip to content

Commit a2320ec

Browse files
grdsdevclaude
andauthored
feat(auth): add OAuth 2.1 client admin endpoints (#809)
Adds OAuth 2.1 client administration endpoints to support managing OAuth clients when the OAuth 2.1 server is enabled in Supabase Auth. New admin.oauth namespace with methods: - listClients() - List all OAuth clients with pagination - createClient() - Create a new OAuth client - getClient() - Get a specific OAuth client - deleteClient() - Delete an OAuth client - regenerateClientSecret() - Regenerate client secret Includes comprehensive types and tests following existing patterns. Ported from supabase-js PR: supabase/supabase-js#1582 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <[email protected]>
1 parent 74c6fe6 commit a2320ec

File tree

4 files changed

+549
-0
lines changed

4 files changed

+549
-0
lines changed

Sources/Auth/AuthAdmin.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ public struct AuthAdmin: Sendable {
1515
var api: APIClient { Dependencies[clientID].api }
1616
var encoder: JSONEncoder { Dependencies[clientID].encoder }
1717

18+
/// Contains all OAuth client administration methods.
19+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
20+
///
21+
/// - Warning: This property requires `service_role` key. Be careful to never expose your `service_role` key in the browser.
22+
public var oauth: AuthAdminOAuth {
23+
AuthAdminOAuth(clientID: clientID)
24+
}
25+
1826
/// Get user by id.
1927
/// - Parameter uid: The user's unique identifier.
2028
/// - Note: This function should only be called on a server. Never expose your `service_role` key in the browser.

Sources/Auth/AuthAdminOAuth.swift

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
//
2+
// AuthAdminOAuth.swift
3+
//
4+
//
5+
// Created by Guilherme Souza on 02/10/25.
6+
//
7+
8+
import Foundation
9+
import HTTPTypes
10+
11+
/// Contains all OAuth client administration methods.
12+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
13+
public struct AuthAdminOAuth: Sendable {
14+
let clientID: AuthClientID
15+
16+
var configuration: AuthClient.Configuration { Dependencies[clientID].configuration }
17+
var api: APIClient { Dependencies[clientID].api }
18+
var encoder: JSONEncoder { Dependencies[clientID].encoder }
19+
20+
/// Lists all OAuth clients with optional pagination.
21+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
22+
///
23+
/// - Note: This function should only be called on a server. Never expose your `service_role` key in the browser.
24+
public func listClients(
25+
params: PageParams? = nil
26+
) async throws -> ListOAuthClientsPaginatedResponse {
27+
struct Response: Decodable {
28+
let clients: [OAuthClient]
29+
let aud: String
30+
}
31+
32+
let httpResponse = try await api.execute(
33+
HTTPRequest(
34+
url: configuration.url.appendingPathComponent("admin/oauth/clients"),
35+
method: .get,
36+
query: [
37+
URLQueryItem(name: "page", value: params?.page?.description ?? ""),
38+
URLQueryItem(name: "per_page", value: params?.perPage?.description ?? ""),
39+
]
40+
)
41+
)
42+
43+
let response = try httpResponse.decoded(as: Response.self, decoder: configuration.decoder)
44+
45+
var pagination = ListOAuthClientsPaginatedResponse(
46+
clients: response.clients,
47+
aud: response.aud,
48+
lastPage: 0,
49+
total: httpResponse.headers[.xTotalCount].flatMap(Int.init) ?? 0
50+
)
51+
52+
let links = httpResponse.headers[.link]?.components(separatedBy: ",") ?? []
53+
if !links.isEmpty {
54+
for link in links {
55+
let page = link.components(separatedBy: ";")[0].components(separatedBy: "=")[1].prefix(
56+
while: \.isNumber
57+
)
58+
let rel = link.components(separatedBy: ";")[1].components(separatedBy: "=")[1]
59+
60+
if rel == "\"last\"", let lastPage = Int(page) {
61+
pagination.lastPage = lastPage
62+
} else if rel == "\"next\"", let nextPage = Int(page) {
63+
pagination.nextPage = nextPage
64+
}
65+
}
66+
}
67+
68+
return pagination
69+
}
70+
71+
/// Creates a new OAuth client.
72+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
73+
///
74+
/// - Note: This function should only be called on a server. Never expose your `service_role` key in the browser.
75+
@discardableResult
76+
public func createClient(params: CreateOAuthClientParams) async throws -> OAuthClient {
77+
try await api.execute(
78+
HTTPRequest(
79+
url: configuration.url.appendingPathComponent("admin/oauth/clients"),
80+
method: .post,
81+
body: encoder.encode(params)
82+
)
83+
)
84+
.decoded(decoder: configuration.decoder)
85+
}
86+
87+
/// Gets details of a specific OAuth client.
88+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
89+
///
90+
/// - Parameter clientId: The unique identifier of the OAuth client.
91+
/// - Note: This function should only be called on a server. Never expose your `service_role` key in the browser.
92+
public func getClient(clientId: String) async throws -> OAuthClient {
93+
try await api.execute(
94+
HTTPRequest(
95+
url: configuration.url.appendingPathComponent("admin/oauth/clients/\(clientId)"),
96+
method: .get
97+
)
98+
)
99+
.decoded(decoder: configuration.decoder)
100+
}
101+
102+
/// Deletes an OAuth client.
103+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
104+
///
105+
/// - Parameter clientId: The unique identifier of the OAuth client to delete.
106+
/// - Note: This function should only be called on a server. Never expose your `service_role` key in the browser.
107+
@discardableResult
108+
public func deleteClient(clientId: String) async throws -> OAuthClient {
109+
try await api.execute(
110+
HTTPRequest(
111+
url: configuration.url.appendingPathComponent("admin/oauth/clients/\(clientId)"),
112+
method: .delete
113+
)
114+
)
115+
.decoded(decoder: configuration.decoder)
116+
}
117+
118+
/// Regenerates the secret for an OAuth client.
119+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
120+
///
121+
/// - Parameter clientId: The unique identifier of the OAuth client.
122+
/// - Note: This function should only be called on a server. Never expose your `service_role` key in the browser.
123+
@discardableResult
124+
public func regenerateClientSecret(clientId: String) async throws -> OAuthClient {
125+
try await api.execute(
126+
HTTPRequest(
127+
url: configuration.url
128+
.appendingPathComponent("admin/oauth/clients/\(clientId)/regenerate_secret"),
129+
method: .post
130+
)
131+
)
132+
.decoded(decoder: configuration.decoder)
133+
}
134+
}

Sources/Auth/Types.swift

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,3 +1027,106 @@ public struct ListUsersPaginatedResponse: Hashable, Sendable {
10271027
// public static let emailChangeCurrent = GenerateLinkType(rawValue: "email_change_current")
10281028
// public static let emailChangeNew = GenerateLinkType(rawValue: "email_change_new")
10291029
//}
1030+
1031+
// MARK: - OAuth Client Types
1032+
1033+
/// OAuth client grant types supported by the OAuth 2.1 server.
1034+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
1035+
public enum OAuthClientGrantType: String, Codable, Hashable, Sendable {
1036+
case authorizationCode = "authorization_code"
1037+
case refreshToken = "refresh_token"
1038+
}
1039+
1040+
/// OAuth client response types supported by the OAuth 2.1 server.
1041+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
1042+
public enum OAuthClientResponseType: String, Codable, Hashable, Sendable {
1043+
case code
1044+
}
1045+
1046+
/// OAuth client type indicating whether the client can keep credentials confidential.
1047+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
1048+
public enum OAuthClientType: String, Codable, Hashable, Sendable {
1049+
case `public`
1050+
case confidential
1051+
}
1052+
1053+
/// OAuth client registration type.
1054+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
1055+
public enum OAuthClientRegistrationType: String, Codable, Hashable, Sendable {
1056+
case dynamic
1057+
case manual
1058+
}
1059+
1060+
/// OAuth client object returned from the OAuth 2.1 server.
1061+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
1062+
public struct OAuthClient: Codable, Hashable, Sendable {
1063+
/// Unique identifier for the OAuth client
1064+
public let clientId: String
1065+
/// Human-readable name of the OAuth client
1066+
public let clientName: String
1067+
/// Client secret (only returned on registration and regeneration)
1068+
public let clientSecret: String?
1069+
/// Type of OAuth client
1070+
public let clientType: OAuthClientType
1071+
/// Token endpoint authentication method
1072+
public let tokenEndpointAuthMethod: String
1073+
/// Registration type of the client
1074+
public let registrationType: OAuthClientRegistrationType
1075+
/// URI of the OAuth client
1076+
public let clientUri: String?
1077+
/// Array of allowed redirect URIs
1078+
public let redirectUris: [String]
1079+
/// Array of allowed grant types
1080+
public let grantTypes: [OAuthClientGrantType]
1081+
/// Array of allowed response types
1082+
public let responseTypes: [OAuthClientResponseType]
1083+
/// Scope of the OAuth client
1084+
public let scope: String?
1085+
/// Timestamp when the client was created
1086+
public let createdAt: Date
1087+
/// Timestamp when the client was last updated
1088+
public let updatedAt: Date
1089+
}
1090+
1091+
/// Parameters for creating a new OAuth client.
1092+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
1093+
public struct CreateOAuthClientParams: Encodable, Hashable, Sendable {
1094+
/// Human-readable name of the OAuth client
1095+
public let clientName: String
1096+
/// URI of the OAuth client
1097+
public let clientUri: String?
1098+
/// Array of allowed redirect URIs
1099+
public let redirectUris: [String]
1100+
/// Array of allowed grant types (optional, defaults to authorization_code and refresh_token)
1101+
public let grantTypes: [OAuthClientGrantType]?
1102+
/// Array of allowed response types (optional, defaults to code)
1103+
public let responseTypes: [OAuthClientResponseType]?
1104+
/// Scope of the OAuth client
1105+
public let scope: String?
1106+
1107+
public init(
1108+
clientName: String,
1109+
clientUri: String? = nil,
1110+
redirectUris: [String],
1111+
grantTypes: [OAuthClientGrantType]? = nil,
1112+
responseTypes: [OAuthClientResponseType]? = nil,
1113+
scope: String? = nil
1114+
) {
1115+
self.clientName = clientName
1116+
self.clientUri = clientUri
1117+
self.redirectUris = redirectUris
1118+
self.grantTypes = grantTypes
1119+
self.responseTypes = responseTypes
1120+
self.scope = scope
1121+
}
1122+
}
1123+
1124+
/// Response type for listing OAuth clients.
1125+
/// Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
1126+
public struct ListOAuthClientsPaginatedResponse: Hashable, Sendable {
1127+
public let clients: [OAuthClient]
1128+
public let aud: String
1129+
public var nextPage: Int?
1130+
public var lastPage: Int
1131+
public var total: Int
1132+
}

0 commit comments

Comments
 (0)