def test_redirect_target(client, default_user): with client.session_transaction() as session: auth.bind_redirect_target("/gh/getsentry/zeus", session=session) assert (auth.get_redirect_target( clear=False, session=session) == "/gh/getsentry/zeus") assert (auth.get_redirect_target( clear=True, session=session) == "/gh/getsentry/zeus") assert auth.get_redirect_target(session=session) is None
def test_login_binds_next_target(client): resp = client.get("/auth/github?next=/gh/github/zeus") assert resp.status_code == 302 location, querystring = resp.headers["Location"].split("?", 1) assert location == GITHUB_AUTH_URI qs = parse_qs(querystring) assert qs["client_id"] == ["github.client-id"] assert qs["redirect_uri"] == ["http://localhost/auth/github/complete"] assert qs["response_type"] == ["code"] assert sorted(qs["scope"][0].split(",")) == ["read:org", "repo", "user:email"] assert auth.get_redirect_target(False) == "/gh/github/zeus"
def test_login_complete_with_next_target(client, db_session, mocker, responses): responses.add( "POST", GITHUB_TOKEN_URI, json={ "token_type": "bearer", "scope": "user:email,read:org", "access_token": "access-token", }, ) # TOOD(dcramer): ideally we could test the header easily, but responses # doesnt make that highly accessible yet responses.add( "GET", "https://api.github.com/user", match_querystring=True, json={"id": 1, "login": "******", "email": "*****@*****.**"}, ) responses.add(responses.GET, "https://api.github.com/user/orgs", json=[]) responses.add( "GET", "https://api.github.com/user/emails", match_querystring=True, json=[ {"email": "*****@*****.**", "verified": True, "primary": True}, {"email": "*****@*****.**", "verified": False, "primary": False}, ], ) with client.session_transaction() as session: auth.bind_redirect_target("/gh/getsentry/zeus", session=session) resp = client.get("/auth/github/complete?code=abc") assert resp.status_code == 302, repr(resp) assert resp.headers["Location"] == "http://localhost/gh/getsentry/zeus" assert auth.get_redirect_target() is None
def get(self): # TODO(dcramer): handle errors oauth = get_oauth_session(state=session.pop("oauth_state", None)) try: resp = oauth.fetch_token( GITHUB_TOKEN_URI, client_secret=current_app.config["GITHUB_CLIENT_SECRET"], authorization_response=request.url, ) except OAuth2Error: current_app.logger.exception("oauth.error") # redirect, as this is likely temporary based on server data return redirect(auth.get_redirect_target(clear=True) or "/") if resp is None or resp.get("access_token") is None: return Response("Access denied: reason=%s error=%s resp=%s" % (request.args["error"], request.args["error_description"], resp)) assert resp.get("token_type") == "bearer" scopes = resp["scope"][0].split(",") if "user:email" not in scopes: raise NotImplementedError # fetch user details client = GitHubClient(token=resp["access_token"]) user_data = client.get("/user") identity_config = { "access_token": resp["access_token"], "refresh_token": resp.get("refresh_token"), "login": user_data["login"], } email_list = client.get("/user/emails") email_list.append({ "email": "{}@users.noreply.github.com".format(user_data["login"]), "verified": True, }) primary_email = user_data.get("email") # HACK(dcramer): capture github's anonymous email addresses when they're not listed # (we haven't actually confirmed they're not listed) if not primary_email: primary_email = next((e["email"] for e in email_list if e["verified"] and e["primary"])) try: # we first attempt to create a new user + identity with db.session.begin_nested(): user = User(email=primary_email) db.session.add(user) identity = Identity( user=user, external_id=str(user_data["id"]), provider="github", scopes=scopes, config=identity_config, ) db.session.add(identity) user_id = user.id new_user = True except IntegrityError: # if that fails, assume the identity exists identity = Identity.query.filter( Identity.external_id == str(user_data["id"]), Identity.provider == "github", ).first() # and if it doesnt, attempt to find a matching user, # as it means the failure above was due to that if not identity: user = User.query.filter(User.email == primary_email).first() assert ( user ) # this should not be possible unless we've got a race condition identity = Identity( user=user, external_id=str(user_data["id"]), provider="github", scopes=scopes, config=identity_config, ) db.session.add(identity) user_id = user.id else: identity.config = identity_config identity.scopes = scopes db.session.add(identity) user_id = identity.user_id new_user = False db.session.flush() for email in email_list: try: with db.session.begin_nested(): db.session.add( Email( user_id=user_id, email=email["email"], verified=email["verified"], )) except IntegrityError: pass db.session.commit() # forcefully expire a session after permanent_session_lifetime # Note: this is enforced in zeus.auth auth.login_user(user_id) user = auth.get_current_user() if new_user: # update synchronously so the new user has a better experience sync_github_access(user_id=user.id) else: sync_github_access.delay(user_id=user.id) next_uri = auth.get_redirect_target(clear=True) or "/" if "/login" in next_uri or "/auth/github" in next_uri: next_uri = "/" return redirect(next_uri)
def get(self): redirect_uri = request.url flow = get_auth_flow(redirect_uri=redirect_uri) try: oauth_response = flow.step2_exchange(request.args['code']) except FlowExchangeError: return redirect('/?auth_error=true') scopes = oauth_response.token_response['scope'].split(',') if 'user:email' not in scopes: raise NotImplementedError # fetch user details github = GitHubClient(token=oauth_response.access_token) user_data = github.get('/user') identity_config = { 'access_token': oauth_response.access_token, 'refresh_token': oauth_response.refresh_token, 'login': user_data['login'], } email_list = github.get('/user/emails') email_list.append({ 'email': '{}@users.noreply.github.com'.format(user_data['login']), 'verified': True, }) primary_email = user_data.get('email') # HACK(dcramer): capture github's anonymous email addresses when they're not listed # (we haven't actually confirmed they're not listed) if not primary_email: primary_email = next((e['email'] for e in email_list if e['verified'] and e['primary'])) try: # we first attempt to create a new user + identity with db.session.begin_nested(): user = User(email=primary_email, ) db.session.add(user) identity = Identity( user=user, external_id=str(user_data['id']), provider='github', scopes=scopes, config=identity_config, ) db.session.add(identity) user_id = user.id except IntegrityError: # if that fails, assume the identity exists identity = Identity.query.filter( Identity.external_id == str(user_data['id']), Identity.provider == 'github', ).first() # and if it doesnt, attempt to find a matching user, # as it means the failure above was due to that if not identity: user = User.query.filter(User.email == primary_email).first() assert user # this should not be possible unless we've got a race condition identity = Identity( user=user, external_id=str(user_data['id']), provider='github', scopes=scopes, config=identity_config, ) db.session.add(identity) user_id = user.id else: identity.config = identity_config identity.scopes = scopes db.session.add(identity) user_id = identity.user_id db.session.flush() for email in email_list: try: with db.session.begin_nested(): db.session.add( Email( user_id=user_id, email=email['email'], verified=email['verified'], )) except IntegrityError: pass db.session.commit() # forcefully expire a session after permanent_session_lifetime # Note: this is enforced in zeus.auth auth.login_user(user_id) # now lets try to update the repos they have access to based on whats # enabled user = auth.get_current_user() grant_access_to_existing_repos(user) return redirect(auth.get_redirect_target(clear=True) or '/')