Skip to content
10 changes: 6 additions & 4 deletions src/auth/infra/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: auth-tests
services:
gotrue: # Signup enabled, autoconfirm off
image: supabase/auth:v2.178.0
image: supabase/auth:v2.180.0
ports:
- '9999:9999'
environment:
Expand Down Expand Up @@ -43,7 +43,7 @@ services:
- db
restart: on-failure
autoconfirm: # Signup enabled, autoconfirm on
image: supabase/auth:v2.178.0
image: supabase/auth:v2.180.0
ports:
- '9998:9998'
environment:
Expand All @@ -70,11 +70,13 @@ services:
GOTRUE_SMTP_PASS: GOTRUE_SMTP_PASS
GOTRUE_SMTP_ADMIN_EMAIL: [email protected]
GOTRUE_COOKIE_KEY: 'sb'
GOTRUE_OAUTH_SERVER_ENABLED: 'true'
GOTRUE_OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION: 'true'
depends_on:
- db
restart: on-failure
autoconfirm_with_asymmetric_keys: # Signup enabled, autoconfirm on
image: supabase/auth:v2.169.0
image: supabase/auth:v2.180.0
ports:
- '9996:9996'
environment:
Expand Down Expand Up @@ -105,7 +107,7 @@ services:
- db
restart: on-failure
disabled: # Signup disabled
image: supabase/auth:v2.178.0
image: supabase/auth:v2.180.0
ports:
- '9997:9997'
environment:
Expand Down
180 changes: 166 additions & 14 deletions src/auth/src/supabase_auth/_async/gotrue_admin_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from pydantic import TypeAdapter

from ..helpers import (
is_valid_uuid,
validate_uuid,
model_validate,
parse_link_response,
parse_user_response,
Expand All @@ -18,15 +18,22 @@
AuthMFAAdminDeleteFactorResponse,
AuthMFAAdminListFactorsParams,
AuthMFAAdminListFactorsResponse,
CreateOAuthClientParams,
GenerateLinkParams,
GenerateLinkResponse,
InviteUserByEmailOptions,
OAuthClient,
OAuthClientListResponse,
OAuthClientResponse,
PageParams,
SignOutScope,
UpdateOAuthClientParams,
User,
UserList,
UserResponse,
)
from .gotrue_admin_mfa_api import AsyncGoTrueAdminMFAAPI
from .gotrue_admin_oauth_api import AsyncGoTrueAdminOAuthAPI
from .gotrue_base_api import AsyncGoTrueBaseAPI


Expand All @@ -50,8 +57,15 @@ def __init__(
)
# TODO(@o-santi): why is is this done this way?
self.mfa = AsyncGoTrueAdminMFAAPI()
self.mfa.list_factors = self._list_factors # type: ignore
self.mfa.delete_factor = self._delete_factor # type: ignore
self.mfa.list_factors = self._list_factors # type: ignore
self.mfa.delete_factor = self._delete_factor # type: ignore
self.oauth = AsyncGoTrueAdminOAuthAPI()
self.oauth.list_clients = self._list_oauth_clients # type: ignore
self.oauth.create_client = self._create_oauth_client # type: ignore
self.oauth.get_client = self._get_oauth_client # type: ignore
self.oauth.update_client = self._update_oauth_client # type: ignore
self.oauth.delete_client = self._delete_oauth_client # type: ignore
self.oauth.regenerate_client_secret = self._regenerate_oauth_client_secret # type: ignore

async def sign_out(self, jwt: str, scope: SignOutScope = "global") -> None:
"""
Expand Down Expand Up @@ -139,7 +153,7 @@ async def get_user_by_id(self, uid: str) -> UserResponse:
This function should only be called on a server.
Never expose your `service_role` key in the browser.
"""
self._validate_uuid(uid)
validate_uuid(uid)

response = await self._request(
"GET",
Expand All @@ -158,7 +172,7 @@ async def update_user_by_id(
This function should only be called on a server.
Never expose your `service_role` key in the browser.
"""
self._validate_uuid(uid)
validate_uuid(uid)
response = await self._request(
"PUT",
f"admin/users/{uid}",
Expand All @@ -173,15 +187,15 @@ async def delete_user(self, id: str, should_soft_delete: bool = False) -> None:
This function should only be called on a server.
Never expose your `service_role` key in the browser.
"""
self._validate_uuid(id)
validate_uuid(id)
body = {"should_soft_delete": should_soft_delete}
await self._request("DELETE", f"admin/users/{id}", body=body)

async def _list_factors(
self,
params: AuthMFAAdminListFactorsParams,
) -> AuthMFAAdminListFactorsResponse:
self._validate_uuid(params.get("user_id"))
validate_uuid(params.get("user_id"))
response = await self._request(
"GET",
f"admin/users/{params.get('user_id')}/factors",
Expand All @@ -192,16 +206,154 @@ async def _delete_factor(
self,
params: AuthMFAAdminDeleteFactorParams,
) -> AuthMFAAdminDeleteFactorResponse:
self._validate_uuid(params.get("user_id"))
self._validate_uuid(params.get("id"))
validate_uuid(params.get("user_id"))
validate_uuid(params.get("id"))
response = await self._request(
"DELETE",
f"admin/users/{params.get('user_id')}/factors/{params.get('id')}",
)
return model_validate(AuthMFAAdminDeleteFactorResponse, response.content)

def _validate_uuid(self, id: str | None) -> None:
if id is None:
raise ValueError("Invalid id, id cannot be none")
if not is_valid_uuid(id):
raise ValueError(f"Invalid id, '{id}' is not a valid uuid")
async def _list_oauth_clients(
self,
params: PageParams | None = None,
) -> OAuthClientListResponse:
"""
Lists all OAuth clients with optional pagination.
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.
"""
if params:
query = QueryParams(page=params.page, per_page=params.per_page)
else:
query = None
response = await self._request(
"GET",
"admin/oauth/clients",
query=query,
no_resolve_json=True,
)

result = model_validate(OAuthClientListResponse, response.content)

# Parse pagination headers
total = response.headers.get("x-total-count")
if total:
result.total = int(total)

links = response.headers.get("link")
if links:
for link in links.split(","):
parts = link.split(";")
if len(parts) >= 2:
page_match = parts[0].split("page=")
if len(page_match) >= 2:
page_num = int(page_match[1].split("&")[0].rstrip(">"))
rel = parts[1].split("=")[1].strip('"')
if rel == "next":
result.next_page = page_num
elif rel == "last":
result.last_page = page_num

return result

async def _create_oauth_client(
self,
params: CreateOAuthClientParams,
) -> OAuthClientResponse:
"""
Creates a new 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.
"""
response = await self._request(
"POST",
"admin/oauth/clients",
body=params,
)

return OAuthClientResponse(
client=model_validate(OAuthClient, response.content)
)
async def _get_oauth_client(
self,
client_id: str,
) -> OAuthClientResponse:
"""
Gets details of a specific 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.
"""
validate_uuid(client_id)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@o-santi I'm using this uuid validation because it was already what was being used by other methods.

Since this is a new feature, does it make sense for the client_id param to be of type uuid.UUID instead of a raw str? So we don't need to validate it? We do this in Swift, but I don't know about Python.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be, I agree with you. But I think for now, we should maintain this behavior for consistency with the other methods, as otherwise the user might need to have some ids in str and others in UUID, which is not really intuitive.

response = await self._request(
"GET",
f"admin/oauth/clients/{client_id}",
)
return OAuthClientResponse(
client=model_validate(OAuthClient, response.content)
)

async def _update_oauth_client(
self,
client_id: str,
params: UpdateOAuthClientParams,
) -> OAuthClientResponse:
"""
Updates an 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.
"""
validate_uuid(client_id)
response = await self._request(
"PUT",
f"admin/oauth/clients/{client_id}",
body=params,
)
return OAuthClientResponse(
client=model_validate(OAuthClient, response.content)
)

async def _delete_oauth_client(
self,
client_id: str,
) -> None:
"""
Deletes an 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.
"""
validate_uuid(client_id)
await self._request(
"DELETE",
f"admin/oauth/clients/{client_id}",
)

async def _regenerate_oauth_client_secret(
self,
client_id: str,
) -> OAuthClientResponse:
"""
Regenerates the secret for an 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.
"""
validate_uuid(client_id)
response = await self._request(
"POST",
f"admin/oauth/clients/{client_id}/regenerate_secret",
)
return OAuthClientResponse(
client=model_validate(OAuthClient, response.content)
)
94 changes: 94 additions & 0 deletions src/auth/src/supabase_auth/_async/gotrue_admin_oauth_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from ..types import (
CreateOAuthClientParams,
OAuthClientListResponse,
OAuthClientResponse,
PageParams,
UpdateOAuthClientParams,
)
from typing import Optional


class AsyncGoTrueAdminOAuthAPI:
"""
Contains all OAuth client administration methods.
Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
"""

async def list_clients(
self,
params: Optional[PageParams] = None,
) -> OAuthClientListResponse:
"""
Lists all OAuth clients with optional pagination.
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.
"""
raise NotImplementedError() # pragma: no cover

async def create_client(
self,
params: CreateOAuthClientParams,
) -> OAuthClientResponse:
"""
Creates a new 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.
"""
raise NotImplementedError() # pragma: no cover

async def get_client(
self,
client_id: str,
) -> OAuthClientResponse:
"""
Gets details of a specific 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.
"""
raise NotImplementedError() # pragma: no cover

async def update_client(
self,
client_id: str,
params: UpdateOAuthClientParams,
) -> OAuthClientResponse:
"""
Updates an 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.
"""
raise NotImplementedError() # pragma: no cover

async def delete_client(
self,
client_id: str,
) -> OAuthClientResponse:
"""
Deletes an 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.
"""
raise NotImplementedError() # pragma: no cover

async def regenerate_client_secret(
self,
client_id: str,
) -> OAuthClientResponse:
"""
Regenerates the secret for an 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.
"""
raise NotImplementedError() # pragma: no cover
Comment on lines +10 to +94
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand this is done to be the same as the other "namespace classes" that already exist in this package, but I believe all of these should hold the actual implementation of the class, instead of having these ugly and confusing placeholders. It can be like this for now, I will refactor this in a later PR.

Loading