Skip to content

Custom GitHub Login URL with Github OAuthenticator Causing State Missing Error #788

@SNagarajan2243

Description

@SNagarajan2243

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions