Description
We are using GitHub as an authentication provider for JupyterHub using OAuthenticator. The authentication flow works perfectly when users are already logged into GitHub, as it directly goes to the authorization URL. However, if a user is not logged in, they are redirected to GitHub's default login URL: https://github.com/login.
To customize this behavior, we have to use our own SSO URL instead of the default GitHub login URL. We attempted to change the login URL to our enterprise SSO login page using the following modified authorization URL:
https://github.com/enterprises/sso?client_id={client_id_value}&return_to=%2Flogin%2Foauth%2Fauthorize%3Fclient_id%3D{client_id_value}%26scope%3Drepo%2Bread%3Auser%2Buser%3Aemail
Where https://github.com/enterprises/sso is our custom SSO login page.
Problem
After successfully logging in and authorizing the GitHub OAuth app, the redirection back to JupyterHub fails.
The error message indicates OAuth state is missing. Try logging in again., even though the state was present during the SSO and authorization process. The error seems to occur in the HubOAuthCallbackHandler class within the auth.py file in JupyterHub, specifically during the state validation process.
async def get(self):
error = self.get_argument("error", False)
if error:
msg = self.get_argument("error_description", error)
raise HTTPError(400, f"Error in oauth: {msg}")
code = self.get_argument("code", False)
if not code:
raise HTTPError(400, "OAuth callback made without a token")
# validate OAuth state
arg_state = self.get_argument("state", None)
if arg_state is None:
raise HTTPError(400, "OAuth state is missing. Try logging in again.")
cookie_name = self.hub_auth.get_state_cookie_name(arg_state)
cookie_state = self.get_secure_cookie(cookie_name)
# clear cookie state now that we've consumed it
if cookie_state:
self.hub_auth.clear_oauth_state_cookies(self)
else:
# completing oauth with stale state, but already logged in.
# stop here and redirect to default URL
# don't complete oauth (no new token), but do complete redirecting to the destination
if self.current_user:
app_log.warning("Attempting oauth completion after already logging in.")
self.hub_auth.clear_oauth_state_cookies(self)
next_url = self.hub_auth.get_next_url(arg_state)
self.redirect(next_url)
return
if isinstance(cookie_state, bytes):
cookie_state = cookie_state.decode('ascii', 'replace')
# check that state matches
if arg_state != cookie_state:
app_log.warning(
"oauth state argument %r != cookie %s=%r",
arg_state,
cookie_name,
cookie_state,
)
raise HTTPError(403, "oauth state does not match. Try logging in again.")
next_url = self.hub_auth.get_next_url(cookie_state)
# clear consumed state from _oauth_states cache now that we're done with it
self.hub_auth.clear_oauth_state(cookie_state)
# clear _all_ oauth state cookies on success
# This prevents multiple concurrent logins in the same browser,
# which is probably okay.
self.hub_auth.clear_oauth_state_cookies(self)
token = await self.hub_auth.token_for_code(code, sync=False)
session_id = self.hub_auth.get_session_id(self)
user_model = await self.hub_auth.user_for_token(
token, session_id=session_id, sync=False
)
if user_model is None:
raise HTTPError(500, "oauth callback failed to identify a user")
app_log.info("Logged-in user %s", user_model['name'])
app_log.debug("User model %s", user_model)
self.hub_auth.set_cookie(self, token)
self.redirect(next_url or self.hub_auth.base_url)
Additionally, we noticed a difference in the structure of the normal login URL and our custom SSO URL:
Custom SSO URL:
https://github.com/enterprises/sso?client_id={client_id_value}&return_to=%2Flogin%2Foauth%2Fauthorize%3Fclient_id%3D{client_id_value}%26scope%3Drepo%2Bread%3Auser%2Buser%3Aemail&response_type=code&redirect_uri=https%3A%2F%2F{domain}%2Fhub%2Foauth_callback&client_id={client_id_value}&code_challenge=AYkpmBxuHKNkP46O3k2B08Sw9okXOS94tvPYULUkdJ8&code_challenge_method=S256&state=eyJzdGF0ZV9pZCI6ICIxZThhMmY2MmFhY2I0ZjUyOTdkYmM3OTczNWVhYWI0ZiJ9
Normal Login URL:
https://github.com/login?client_id={client_id_value}&return_to=%2Flogin%2Foauth%2Fauthorize%3Fclient_id%3D{client_id_value}%26code_challenge%3Dh4l4aUSMoAxf5DQYa2vnLZQIi500IpWXEoLLo_TzW_g%26code_challenge_method%3DS256%26redirect_uri%3Dhttps%253A%252F%252F{domain}%252Fhub%252Foauth_callback%26response_type%3Dcode%26state%3DeyJzdGF0ZV9pZCI6ICIwZTcwZDhhMzY5NzY0NzIwOTc0MTFkNDQ4NjNlNGE2NCJ9
This structural difference may be contributing to the loss of the state parameter.
What We Have Tried
- Confirmed the state parameter was generated and included in the authorization URL.
- Ensured the callback URL was correctly set in the GitHub OAuth app.
- Verified that the state was present before redirection.
Question
Is there a recommended way to configure OAuthenticator to change the GitHub login URL without causing the state missing error? Alternatively, is there any other approach to achieve a custom login URL while maintaining the OAuth state?
Description
We are using GitHub as an authentication provider for JupyterHub using OAuthenticator. The authentication flow works perfectly when users are already logged into GitHub, as it directly goes to the authorization URL. However, if a user is not logged in, they are redirected to GitHub's default login URL:
https://github.com/login.To customize this behavior, we have to use our own SSO URL instead of the default GitHub login URL. We attempted to change the login URL to our enterprise SSO login page using the following modified authorization URL:
https://github.com/enterprises/sso?client_id={client_id_value}&return_to=%2Flogin%2Foauth%2Fauthorize%3Fclient_id%3D{client_id_value}%26scope%3Drepo%2Bread%3Auser%2Buser%3AemailWhere
https://github.com/enterprises/ssois our custom SSO login page.Problem
After successfully logging in and authorizing the GitHub OAuth app, the redirection back to JupyterHub fails.
The error message indicates
OAuth state is missing. Try logging in again., even though the state was present during the SSO and authorization process. The error seems to occur in theHubOAuthCallbackHandlerclass within theauth.pyfile inJupyterHub, specifically during the state validation process.Additionally, we noticed a difference in the structure of the normal login URL and our custom SSO URL:
Custom SSO URL:
Normal Login URL:
This structural difference may be contributing to the loss of the state parameter.
What We Have Tried
Question
Is there a recommended way to configure OAuthenticator to change the GitHub login URL without causing the state missing error? Alternatively, is there any other approach to achieve a custom login URL while maintaining the OAuth state?