Coverage for polar/oauth2/schemas.py: 90%
68 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 12:42 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 12:42 +0000
1import ipaddress
2import re
3from typing import Annotated, Any, Literal
5from fastapi.openapi.constants import REF_TEMPLATE
6from pydantic import (
7 UUID4,
8 AfterValidator,
9 AnyUrl,
10 BeforeValidator,
11 Discriminator,
12 EmailStr,
13 Field,
14 HttpUrl,
15 TypeAdapter,
16)
18from polar.auth.scope import SCOPES_SUPPORTED, Scope, scope_to_list
19from polar.kit.schemas import Schema, TimestampedSchema
21from .sub_type import SubType
23_LOCALHOST_HOST_PATTERN = re.compile(r"([^\.]+\.)?localhost(\d+)?", flags=re.IGNORECASE)
26def _is_localhost(host: str) -> bool:
27 try:
28 return ipaddress.IPv4Address(host).is_private
29 except ValueError:
30 return _LOCALHOST_HOST_PATTERN.match(host) is not None
33def _is_https_or_localhost(value: HttpUrl) -> HttpUrl:
34 if value.scheme == "http" and (value.host is None or not _is_localhost(value.host)): 34 ↛ 35line 34 didn't jump to line 35 because the condition on line 34 was never true
35 raise ValueError("An HTTPS URL is required.")
36 return value
39URIOrLocalhost = Annotated[AnyUrl, AfterValidator(_is_https_or_localhost)]
40Scopes = Annotated[list[Scope], BeforeValidator(scope_to_list)]
43class OAuth2ClientConfiguration(Schema):
44 redirect_uris: list[URIOrLocalhost]
45 token_endpoint_auth_method: Literal[
46 "client_secret_basic", "client_secret_post", "none"
47 ] = "client_secret_post"
48 grant_types: list[Literal["authorization_code", "refresh_token"]] = [
49 "authorization_code",
50 "refresh_token",
51 ]
52 response_types: list[Literal["code"]] = ["code"]
53 scope: str = " ".join(SCOPES_SUPPORTED)
54 client_name: str
55 client_uri: str | None = None
56 logo_uri: HttpUrl | None = None
57 tos_uri: HttpUrl | None = None
58 policy_uri: HttpUrl | None = None
59 default_sub_type: SubType = SubType.organization
62class OAuth2ClientConfigurationUpdate(OAuth2ClientConfiguration):
63 client_id: str
66class OAuth2Client(TimestampedSchema, OAuth2ClientConfiguration):
67 client_id: str
68 client_secret: str
69 client_id_issued_at: int
70 client_secret_expires_at: int
73class OAuth2ClientPublic(TimestampedSchema):
74 client_id: str
75 client_name: str | None
76 client_uri: str | None
77 logo_uri: str | None
78 tos_uri: str | None
79 policy_uri: str | None
82class AuthorizeUser(Schema):
83 id: UUID4
84 email: EmailStr
85 avatar_url: str | None
88class AuthorizeOrganization(Schema):
89 id: UUID4
90 slug: str
91 avatar_url: str | None
94class AuthorizeResponseBase(Schema):
95 client: OAuth2ClientPublic
96 sub_type: SubType
97 sub: AuthorizeUser | AuthorizeOrganization | None
98 scopes: Scopes
101class AuthorizeResponseUser(AuthorizeResponseBase):
102 sub_type: Literal[SubType.user]
103 sub: AuthorizeUser | None
106class AuthorizeResponseOrganization(AuthorizeResponseBase):
107 sub_type: Literal[SubType.organization]
108 sub: AuthorizeOrganization | None
109 organizations: list[AuthorizeOrganization]
112AuthorizeResponse = Annotated[
113 AuthorizeResponseUser | AuthorizeResponseOrganization,
114 Discriminator(discriminator="sub_type"),
115]
117authorize_response_adapter: TypeAdapter[AuthorizeResponse] = TypeAdapter(
118 AuthorizeResponse
119)
122class TokenRequestBase(Schema):
123 grant_type: Literal["authorization_code", "refresh_token", "web"]
124 client_id: str
125 client_secret: str
128class AuthorizationCodeTokenRequest(TokenRequestBase):
129 grant_type: Literal["authorization_code"]
130 code: str
131 redirect_uri: HttpUrl
134class RefreshTokenRequest(TokenRequestBase):
135 grant_type: Literal["refresh_token"]
137 refresh_token: str
140class WebTokenRequest(TokenRequestBase):
141 grant_type: Literal["web"]
142 session_token: str
143 sub_type: Literal["user", "organization"] = Field(default="user")
144 sub: UUID4 | None = None
145 scope: str | None = Field(default=None)
148class TokenResponse(Schema):
149 access_token: str
150 token_type: Literal["Bearer"]
151 expires_in: int
152 refresh_token: str | None
153 scope: str
154 id_token: str
157class RevokeTokenRequest(Schema):
158 token: str
159 token_type_hint: Literal["access_token", "refresh_token"] | None = None
160 client_id: str
161 client_secret: str
164class RevokeTokenResponse(Schema): ... 164 ↛ 167line 164 didn't jump to line 167 because
167class IntrospectTokenRequest(Schema):
168 token: str
169 token_type_hint: Literal["access_token", "refresh_token"] | None = None
170 client_id: str
171 client_secret: str
174class IntrospectTokenResponse(Schema):
175 active: bool
176 client_id: str
177 token_type: Literal["access_token", "refresh_token"]
178 scope: str
179 sub_type: SubType
180 sub: str
181 aud: str
182 iss: str
183 exp: int
184 iat: int
187class UserInfoUser(Schema):
188 sub: str
189 name: str | None = None
190 email: str | None = None
191 email_verified: bool | None = None
194class UserInfoOrganization(Schema):
195 sub: str
196 name: str | None = None
199UserInfo = UserInfoUser | UserInfoOrganization
202def add_oauth2_form_schemas(openapi_schema: dict[str, Any]) -> dict[str, Any]:
203 openapi_schema["components"]["schemas"]["AuthorizationCodeTokenRequest"] = (
204 AuthorizationCodeTokenRequest.model_json_schema(ref_template=REF_TEMPLATE)
205 )
206 openapi_schema["components"]["schemas"]["RefreshTokenRequest"] = (
207 RefreshTokenRequest.model_json_schema(ref_template=REF_TEMPLATE)
208 )
209 openapi_schema["components"]["schemas"]["WebTokenRequest"] = (
210 WebTokenRequest.model_json_schema(ref_template=REF_TEMPLATE)
211 )
212 openapi_schema["components"]["schemas"]["RevokeTokenRequest"] = (
213 RevokeTokenRequest.model_json_schema(ref_template=REF_TEMPLATE)
214 )
215 openapi_schema["components"]["schemas"]["IntrospectTokenRequest"] = (
216 IntrospectTokenRequest.model_json_schema(ref_template=REF_TEMPLATE)
217 )
218 return openapi_schema