Repository: jupyterhub/jupyterhub

Issue:
JH 0.8.1 -> 0.9.2 upgrade `invalid_redirect_uri` error for running users
## Z2JH deployment upgrade failure for running users
We recently released a new Z2JH Helm chart. Apparently, users with a running pod revisiting their server through the hub got to see the error below, if an upgrade from `v0.6` to `0.7.0` had been made in between visits of the running pod. The suspected change that could have caused this is that JupyterHub went from `0.8.1` to `0.9.2`, and the `--upgrade-db` flag (if I recall correctly) did some work when the JupyterHub started again.

```
{“error”: “invalid_redirect_uri”, “error_description”: “Invalid redirect URI”}
```

@minrk had a clue as to why this happened
> It means the db upgrade resulted in invalid oauth for the running users.
I think this might be something to fix in the jupyterhub alembic upgrades


Discussion:
@consideRatio, @minrk is this addressed enough to close?
No @willingc, it remains very relevant, and perhaps one of our more pressing matters for the z2jh repo as it caused / cause quite some pain for users atm.

I recall finding a bug with a key being lost because it wasn't properly read from the configmap or similar. @minrk do you know if a certain `openssl rand -hex 32` secret being reset could cause this issue? What could trigger such potential reset in a db upgrade as compared to just hub restarts? Hmmm may be a dead end investigating this but I put it out there anyhow for now.

Resolving patch:
diff --git a/jupyterhub/app.py b/jupyterhub/app.py
--- a/jupyterhub/app.py
+++ b/jupyterhub/app.py
@@ -1506,6 +1506,10 @@ def cleanup_oauth_clients(self):
         for user in self.users.values():
             for spawner in user.spawners.values():
                 oauth_client_ids.add(spawner.oauth_client_id)
+                # avoid deleting clients created by 0.8
+                # 0.9 uses `jupyterhub-user-...` for the client id, while
+                # 0.8 uses just `user-...`
+                oauth_client_ids.add(spawner.oauth_client_id.split('-', 1)[1])
 
         for i, oauth_client in enumerate(self.db.query(orm.OAuthClient)):
             if oauth_client.identifier not in oauth_client_ids:
diff --git a/jupyterhub/user.py b/jupyterhub/user.py
--- a/jupyterhub/user.py
+++ b/jupyterhub/user.py
@@ -554,11 +554,25 @@ async def stop(self, server_name=''):
             # remove server entry from db
             spawner.server = None
             if not spawner.will_resume:
-                # find and remove the API token if the spawner isn't
+                # find and remove the API token and oauth client if the spawner isn't
                 # going to re-use it next time
                 orm_token = orm.APIToken.find(self.db, api_token)
                 if orm_token:
                     self.db.delete(orm_token)
+                # remove oauth client as well
+                # handle upgrades from 0.8, where client id will be `user-USERNAME`,
+                # not just `jupyterhub-user-USERNAME`
+                client_ids = (
+                    spawner.oauth_client_id,
+                    spawner.oauth_client_id.split('-', 1)[1],
+                )
+                for oauth_client in (
+                    self.db
+                        .query(orm.OAuthClient)
+                        .filter(orm.OAuthClient.identifier.in_(client_ids))
+                ):
+                    self.log.debug("Deleting oauth client %s", oauth_client.identifier)
+                    self.db.delete(oauth_client)
             self.db.commit()
         finally:
             spawner.orm_spawner.started = None


Test patch: