Spaces:
Running
Running
Commit ·
f3ab5f0
1
Parent(s): efa9f90
feat: warning
Browse files- app/api/v1/google_oauth.py +44 -1
- app/models/schemas.py +3 -0
app/api/v1/google_oauth.py
CHANGED
|
@@ -55,6 +55,42 @@ def _http_error(exc: GoogleOAuthError) -> HTTPException:
|
|
| 55 |
return HTTPException(status_code=exc.status_code, detail=exc.message)
|
| 56 |
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
@router.post("/auth-url", response_model=GoogleOAuthAuthUrlResponse,
|
| 59 |
summary="Generate a Google OAuth authorization URL (Step 1)")
|
| 60 |
async def create_auth_url(
|
|
@@ -78,7 +114,14 @@ async def create_auth_url(
|
|
| 78 |
body.client_id[:8],
|
| 79 |
(time.perf_counter() - start) * 1000,
|
| 80 |
)
|
| 81 |
-
return GoogleOAuthAuthUrlResponse(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
@router.post("/callback", response_model=GoogleOAuthTokenResponse,
|
|
|
|
| 55 |
return HTTPException(status_code=exc.status_code, detail=exc.message)
|
| 56 |
|
| 57 |
|
| 58 |
+
# Google's basic OpenID profile scopes. A "Testing" consent screen does NOT
|
| 59 |
+
# apply the 7-day refresh-token expiry when only these scopes are requested.
|
| 60 |
+
_BASIC_PROFILE_SCOPES = frozenset({
|
| 61 |
+
"openid",
|
| 62 |
+
"email",
|
| 63 |
+
"profile",
|
| 64 |
+
"https://www.googleapis.com/auth/userinfo.email",
|
| 65 |
+
"https://www.googleapis.com/auth/userinfo.profile",
|
| 66 |
+
})
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _refresh_token_warning(*, access_type: Optional[str], scope: str) -> Optional[str]:
|
| 70 |
+
"""Return a user-facing warning about the refresh token this flow will issue.
|
| 71 |
+
|
| 72 |
+
Mirrors Google's documented refresh-token expiration rules:
|
| 73 |
+
https://developers.google.com/identity/protocols/oauth2#expiration
|
| 74 |
+
"""
|
| 75 |
+
if access_type != "offline":
|
| 76 |
+
return (
|
| 77 |
+
"No refresh token will be issued because access_type is not 'offline'. "
|
| 78 |
+
"Set access_type to 'offline' to receive a long-lived refresh token "
|
| 79 |
+
"(issued only on the user's first authorization)."
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
requested_scopes = {s.strip() for s in scope.split() if s.strip()}
|
| 83 |
+
testing_note = ""
|
| 84 |
+
if not requested_scopes.issubset(_BASIC_PROFILE_SCOPES):
|
| 85 |
+
testing_note = (
|
| 86 |
+
" However, it expires after 7 days if the OAuth consent screen is in "
|
| 87 |
+
"'Testing' publishing status."
|
| 88 |
+
)
|
| 89 |
+
return (
|
| 90 |
+
"The refresh token is long-lived and does not expire on its own."
|
| 91 |
+
) + testing_note
|
| 92 |
+
|
| 93 |
+
|
| 94 |
@router.post("/auth-url", response_model=GoogleOAuthAuthUrlResponse,
|
| 95 |
summary="Generate a Google OAuth authorization URL (Step 1)")
|
| 96 |
async def create_auth_url(
|
|
|
|
| 114 |
body.client_id[:8],
|
| 115 |
(time.perf_counter() - start) * 1000,
|
| 116 |
)
|
| 117 |
+
return GoogleOAuthAuthUrlResponse(
|
| 118 |
+
success=True,
|
| 119 |
+
auth_url=auth_url,
|
| 120 |
+
state=state,
|
| 121 |
+
warning=_refresh_token_warning(
|
| 122 |
+
access_type=body.access_type, scope=body.scope
|
| 123 |
+
),
|
| 124 |
+
)
|
| 125 |
|
| 126 |
|
| 127 |
@router.post("/callback", response_model=GoogleOAuthTokenResponse,
|
app/models/schemas.py
CHANGED
|
@@ -915,6 +915,9 @@ class GoogleOAuthAuthUrlResponse(BaseModel):
|
|
| 915 |
success: bool
|
| 916 |
auth_url: str
|
| 917 |
state: str
|
|
|
|
|
|
|
|
|
|
| 918 |
error: Optional[str] = None
|
| 919 |
|
| 920 |
|
|
|
|
| 915 |
success: bool
|
| 916 |
auth_url: str
|
| 917 |
state: str
|
| 918 |
+
warning: Optional[str] = Field(
|
| 919 |
+
None, description="User-facing warning describing refresh-token issuance and validity"
|
| 920 |
+
)
|
| 921 |
error: Optional[str] = None
|
| 922 |
|
| 923 |
|