Skip to content

Commit 9287024

Browse files
grdsdevclaude
andcommitted
feat(auth): add OAuth 2.1 client admin endpoints
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 de1b8da commit 9287024

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)