diff --git a/src/google/adk/auth/auth_credential.py b/src/google/adk/auth/auth_credential.py index 4a2add823c..9af9713e6d 100644 --- a/src/google/adk/auth/auth_credential.py +++ b/src/google/adk/auth/auth_credential.py @@ -98,6 +98,15 @@ class OAuth2Auth(BaseModelWithConfig): ] | None ) = "client_secret_basic" + # The OAuth2 ``prompt`` parameter forwarded to the authorization endpoint. + # When ``None`` (default), ``"consent"`` is sent to preserve existing + # behavior. Standard values per RFC 6749 / OIDC: ``"none"``, ``"login"``, + # ``"consent"``, ``"select_account"``. ``str`` is also allowed so that + # IdP-specific values (e.g. Azure's ``"admin_consent"``) can be passed + # through unchanged. + prompt: Literal["none", "login", "consent", "select_account"] | str | None = ( + None + ) class ServiceAccountCredential(BaseModelWithConfig): diff --git a/src/google/adk/auth/auth_handler.py b/src/google/adk/auth/auth_handler.py index 8e8f5d340b..36c82a7e84 100644 --- a/src/google/adk/auth/auth_handler.py +++ b/src/google/adk/auth/auth_handler.py @@ -197,7 +197,7 @@ def generate_auth_uri( ) params = { "access_type": "offline", - "prompt": "consent", + "prompt": auth_credential.oauth2.prompt or "consent", } if auth_credential.oauth2.audience: params["audience"] = auth_credential.oauth2.audience diff --git a/tests/unittests/auth/test_auth_handler.py b/tests/unittests/auth/test_auth_handler.py index c19a5d93fd..ad1f870df5 100644 --- a/tests/unittests/auth/test_auth_handler.py +++ b/tests/unittests/auth/test_auth_handler.py @@ -66,6 +66,8 @@ def create_authorization_url(self, url, **kwargs): params = f"client_id={self.client_id}&scope={self.scope}" if kwargs.get("audience"): params += f"&audience={kwargs.get('audience')}" + if kwargs.get("prompt"): + params += f"&prompt={kwargs.get('prompt')}" return f"{url}?{params}", "mock_state" def fetch_token( @@ -250,6 +252,75 @@ def test_generate_auth_uri_with_audience_and_prompt( result = handler.generate_auth_uri() assert "audience=test_audience" in result.oauth2.auth_uri + # When prompt is unset, the default "consent" must be forwarded. + assert "prompt=consent" in result.oauth2.auth_uri + + @patch("google.adk.auth.auth_handler.OAuth2Session", MockOAuth2Session) + def test_generate_auth_uri_default_prompt_is_consent(self, auth_config): + """When OAuth2Auth.prompt is unset, the auth URI must send prompt=consent. + + Locks in backward-compatible behavior — existing callers that never set + a prompt continue to get the consent screen. + """ + handler = AuthHandler(auth_config) + result = handler.generate_auth_uri() + + assert "prompt=consent" in result.oauth2.auth_uri + + @patch("google.adk.auth.auth_handler.OAuth2Session", MockOAuth2Session) + def test_generate_auth_uri_with_custom_prompt_none( + self, openid_auth_scheme, oauth2_credentials + ): + """A caller-supplied prompt value must override the default.""" + oauth2_credentials.oauth2.prompt = "none" + exchanged = oauth2_credentials.model_copy(deep=True) + + config = AuthConfig( + auth_scheme=openid_auth_scheme, + raw_auth_credential=oauth2_credentials, + exchanged_auth_credential=exchanged, + ) + handler = AuthHandler(config) + result = handler.generate_auth_uri() + + assert "prompt=none" in result.oauth2.auth_uri + assert "prompt=consent" not in result.oauth2.auth_uri + + @patch("google.adk.auth.auth_handler.OAuth2Session", MockOAuth2Session) + def test_generate_auth_uri_with_custom_prompt_select_account( + self, openid_auth_scheme, oauth2_credentials + ): + """Standard OIDC prompt values other than 'consent' must pass through.""" + oauth2_credentials.oauth2.prompt = "select_account" + exchanged = oauth2_credentials.model_copy(deep=True) + + config = AuthConfig( + auth_scheme=openid_auth_scheme, + raw_auth_credential=oauth2_credentials, + exchanged_auth_credential=exchanged, + ) + handler = AuthHandler(config) + result = handler.generate_auth_uri() + + assert "prompt=select_account" in result.oauth2.auth_uri + + @patch("google.adk.auth.auth_handler.OAuth2Session", MockOAuth2Session) + def test_generate_auth_uri_with_idp_specific_prompt( + self, openid_auth_scheme, oauth2_credentials + ): + """IdP-specific prompt values (e.g. Azure's admin_consent) pass through.""" + oauth2_credentials.oauth2.prompt = "admin_consent" + exchanged = oauth2_credentials.model_copy(deep=True) + + config = AuthConfig( + auth_scheme=openid_auth_scheme, + raw_auth_credential=oauth2_credentials, + exchanged_auth_credential=exchanged, + ) + handler = AuthHandler(config) + result = handler.generate_auth_uri() + + assert "prompt=admin_consent" in result.oauth2.auth_uri @patch("google.adk.auth.auth_handler.OAuth2Session", MockOAuth2Session) def test_generate_auth_uri_openid(