Skip to content

Commit 4b1d697

Browse files
Merge pull request #4674 from communitybridge/unicron-github-oauth-app-redirect
Rewrite /v2/user-from-session to use already existing callback to avoid need of updating GitHub OAuth app
2 parents 72420a0 + 3039bb6 commit 4b1d697

File tree

6 files changed

+118
-124
lines changed

6 files changed

+118
-124
lines changed

cla-backend/cla/controllers/repository_service.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"""
77

88
import cla
9-
from falcon import HTTP_202, HTTP_404
9+
from falcon import HTTP_404
1010

1111
def received_activity(provider, data):
1212
"""
@@ -34,19 +34,20 @@ def sign_request(provider, installation_id, github_repository_id, change_request
3434
service = cla.utils.get_repository_service(provider)
3535
return service.sign_request(installation_id, github_repository_id, change_request_id, request)
3636

37-
def user_from_session(redirect, redirect_url, state, code, request, response=None):
37+
def user_from_session(request, response=None):
3838
"""
3939
Return user from OAuth2 session
4040
"""
41-
# LG: to test using MockGitHub class
41+
# LG: to test with other GitHub APP and BASE API URL (for OAuth redirects)
4242
# import os
43+
# os.environ["GH_OAUTH_CLIENT_ID"] = os.getenv("GH_OAUTH_CLIENT_ID_CLI", os.environ["GH_OAUTH_CLIENT_ID"])
44+
# os.environ["GH_OAUTH_SECRET"] = os.getenv("GH_OAUTH_SECRET_CLI", os.environ["GH_OAUTH_SECRET"])
45+
# os.environ["CLA_API_BASE"] = os.getenv("CLA_API_BASE_CLI", os.environ["CLA_API_BASE"])
46+
# LG: to test using MockGitHub class
4347
# from cla.models.github_models import MockGitHub
44-
# user = MockGitHub(os.environ["GITHUB_OAUTH_TOKEN"]).user_from_session(request, redirect, redirect_url, state, code)
45-
user = cla.utils.get_repository_service('github').user_from_session(request, redirect, redirect_url, state, code)
48+
# user = MockGitHub(os.environ["GITHUB_OAUTH_TOKEN"]).user_from_session(request)
49+
user = cla.utils.get_repository_service('github').user_from_session(request)
4650
if user is None:
4751
response.status = HTTP_404
4852
return {"errors": "Cannot find user from session"}
49-
if isinstance(user, dict):
50-
response.status = HTTP_202
51-
return user
5253
return user.to_dict()

cla-backend/cla/models/github_models.py

Lines changed: 56 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import concurrent.futures
88
import json
99
import os
10+
import base64
11+
import binascii
1012
import threading
1113
import time
1214
import uuid
@@ -96,53 +98,29 @@ def received_activity(self, data):
9698
else:
9799
cla.log.debug("github_models.received_activity - Ignoring unsupported action: {}".format(data["action"]))
98100

99-
def user_from_session(self, request, redirect, redirect_url, state, code):
101+
def user_from_session(self, request):
100102
fn = "github_models.user_from_session"
101-
cla.log.debug(f"{fn} - Loading session from request: {request}...")
103+
cla.log.debug(f"{fn} - loading session from request: {request}...")
102104
session = self._get_request_session(request)
103-
cla.log.debug(f"{fn} - redirect: {redirect}, redirect_url: {redirect_url}, state: {state}, code: {code}, session: {session}")
105+
cla.log.debug(f"{fn} - session: {session}")
104106

105-
# we can already have token in the session
107+
# We can already have token in the session
106108
if "github_oauth2_token" in session:
107-
cla.log.debug(f"{fn} - Using existing session GitHub OAuth2 token")
109+
cla.log.debug(f"{fn} - using existing session GitHub OAuth2 token")
108110
user = self.get_or_create_user(request)
109-
cla.log.debug(f"{fn} - loaded user {user.to_dict()}")
110-
return user
111-
112-
# if not then we can either request a new OAuth2 GitHub authentication or user code & state from GitHub to create a session
113-
if code and state:
114-
session_state = None
115-
if "github_oauth2_state" in session:
116-
session_state = session["github_oauth2_state"]
117-
cla.log.warning(f"{fn} - github_oauth2_state in current session: {session_state}")
111+
if user is None:
112+
cla.log.debug(f"{fn} - cannot find user, returning HTTP 404 status")
118113
else:
119-
cla.log.warning(f"{fn} - github_oauth2_state not set in current session")
120-
if session_state and state != session_state:
121-
cla.log.warning(f"{fn} - invalid GitHub OAuth2 state {session_state} expecting {state}")
122-
raise falcon.HTTPBadRequest(f"Invalid OAuth2 state: '{session_state}' != '{state}'")
123-
token_url = cla.conf["GITHUB_OAUTH_TOKEN_URL"]
124-
client_id = os.environ["GH_OAUTH_CLIENT_ID"]
125-
client_secret = os.environ["GH_OAUTH_SECRET"]
126-
try:
127-
token = self._fetch_token(client_id, state, token_url, client_secret, code)
128-
except Exception as err:
129-
cla.log.warning(f"{fn} - GitHub OAuth2 error: {err}. Likely bad or expired code.")
130-
raise falcon.HTTPBadRequest("OAuth2 code is invalid or expired")
131-
cla.log.debug(f"{fn} - oauth2 token received for state {state}: {token} - storing token in session")
132-
session["github_oauth2_token"] = token
133-
user = self.get_or_create_user(request)
134-
cla.log.debug(f"{fn} - loaded user {user.to_dict()}")
114+
cla.log.debug(f"{fn} - loaded user {user.to_dict()} returning HTTP 200 status")
135115
return user
136-
else:
137-
cla.log.debug(f"{fn} - No existing GitHub OAuth2 token - building authorization url and state")
138-
authorization_url, new_state = self.get_github_oauth2_redirect_url_and_state(redirect_url)
139-
cla.log.debug(f"{fn} - Obtained GitHub OAuth2 state from authorization - storing state in the session...")
140-
session["github_oauth2_state"] = new_state
141-
cla.log.debug(f"{fn} - GitHub OAuth2 request with state {new_state} - sending user to {authorization_url}")
142-
if redirect:
143-
raise falcon.HTTPFound(authorization_url)
144-
else:
145-
return { "redirect_url": authorization_url }
116+
117+
authorization_url, csrf_token = self.get_authorization_url_and_state(None, None, None, ["user:email"], state='user-from-session')
118+
cla.log.debug(f"{fn} - obtained GitHub OAuth2 state from authorization - storing CSRF token in the session...")
119+
session["github_oauth2_state"] = csrf_token
120+
cla.log.debug(f"{fn} - GitHub OAuth2 request with CSRF token {csrf_token} - sending user to {authorization_url}")
121+
cla.log.debug(f"{fn} - redirecting by returning 302 and redirect URL")
122+
# We must redirect to GitHub OAuth app for authentication, it will return you to /v2/github/installation which will handle returning user data
123+
raise falcon.HTTPFound(authorization_url)
146124

147125
def sign_request(self, installation_id, github_repository_id, change_request_id, request):
148126
"""
@@ -204,26 +182,7 @@ def _get_request_session(self, request) -> dict: # pylint: disable=no-self-use
204182

205183
return session
206184

207-
def get_github_oauth2_redirect_url_and_state(self, redirect_uri):
208-
fn = "github_models.get_github_oauth2_redirect_url_and_state"
209-
github_oauth_url = cla.conf["GITHUB_OAUTH_AUTHORIZE_URL"]
210-
github_oauth_client_id = os.environ["GH_OAUTH_CLIENT_ID"]
211-
if not redirect_uri:
212-
redirect_uri = os.environ.get("CLA_API_BASE", "").strip() + "/v2/user-from-session"
213-
214-
scope = ["user:email"]
215-
cla.log.debug(
216-
f"{fn} - Directing user to the github authorization url: {github_oauth_url} via "
217-
f"our github installation flow: {redirect_uri} "
218-
f"using the github oauth client id: {github_oauth_client_id[0:5]} "
219-
f"with scope: {scope}"
220-
)
221-
222-
return self._get_authorization_url_and_state(
223-
client_id=github_oauth_client_id, redirect_uri=redirect_uri, scope=scope, authorize_url=github_oauth_url
224-
)
225-
226-
def get_authorization_url_and_state(self, installation_id, github_repository_id, pull_request_number, scope):
185+
def get_authorization_url_and_state(self, installation_id, github_repository_id, pull_request_number, scope, state=None):
227186
"""
228187
Helper method to get the GitHub OAuth2 authorization URL and state.
229188
@@ -254,14 +213,14 @@ def get_authorization_url_and_state(self, installation_id, github_repository_id,
254213
)
255214

256215
return self._get_authorization_url_and_state(
257-
client_id=github_oauth_client_id, redirect_uri=redirect_uri, scope=scope, authorize_url=github_oauth_url
216+
client_id=github_oauth_client_id, redirect_uri=redirect_uri, scope=scope, authorize_url=github_oauth_url, state=state,
258217
)
259218

260-
def _get_authorization_url_and_state(self, client_id, redirect_uri, scope, authorize_url):
219+
def _get_authorization_url_and_state(self, client_id, redirect_uri, scope, authorize_url, state=None):
261220
"""
262221
Mockable helper method to do the fetching of the authorization URL and state from GitHub.
263222
"""
264-
return cla.utils.get_authorization_url_and_state(client_id, redirect_uri, scope, authorize_url)
223+
return cla.utils.get_authorization_url_and_state(client_id, redirect_uri, scope, authorize_url, state)
265224

266225
def oauth2_redirect(self, state, code, request): # pylint: disable=too-many-arguments
267226
"""
@@ -283,8 +242,38 @@ def oauth2_redirect(self, state, code, request): # pylint: disable=too-many-arg
283242
cla.log.warning(f"{fn} - github_oauth2_state not set in current session")
284243

285244
if state != session_state:
286-
cla.log.warning(f"{fn} - invalid GitHub OAuth2 state {session_state} expecting {state}")
287-
raise falcon.HTTPBadRequest("Invalid OAuth2 state", state)
245+
# Eventually handle user-from-session API callback
246+
try:
247+
state_data = json.loads(base64.urlsafe_b64decode(state.encode()).decode())
248+
except (ValueError, json.JSONDecodeError, binascii.Error):
249+
cla.log.warning(f"{fn} - failed to decode state: {state}, error: {err}")
250+
raise falcon.HTTPBadRequest("Invalid OAuth2 state", state)
251+
state_token = state_data["csrf"]
252+
value = state_data["state"]
253+
if value != "user-from-session":
254+
cla.log.warning(f"{fn} - invalid GitHub OAuth2 state {session_state} expecting {state}, value: {value}")
255+
raise falcon.HTTPBadRequest("Invalid OAuth2 state", state)
256+
if state_token != session_state:
257+
cla.log.warning(f"{fn} - invalid GitHub OAuth2 state {session_state} expecting {state_token} while handling user-from-session callback")
258+
raise falcon.HTTPBadRequest(f"Invalid OAuth2 state")
259+
cla.log.debug(f"handling user-from-session callback")
260+
token_url = cla.conf["GITHUB_OAUTH_TOKEN_URL"]
261+
client_id = os.environ["GH_OAUTH_CLIENT_ID"]
262+
cla.log.debug(f"{fn} - using client ID {client_id}")
263+
client_secret = os.environ["GH_OAUTH_SECRET"]
264+
try:
265+
token = self._fetch_token(client_id, state, token_url, client_secret, code)
266+
except Exception as err:
267+
cla.log.warning(f"{fn} - GitHub OAuth2 error: {err}. Likely bad or expired code, returning HTTP 404 state.")
268+
raise falcon.HTTPBadRequest("OAuth2 code is invalid or expired")
269+
cla.log.debug(f"{fn} - oauth2 token received for state {state}: {token} - storing token in session")
270+
session["github_oauth2_token"] = token
271+
user = self.get_or_create_user(request)
272+
if user is None:
273+
cla.log.debug(f"{fn} - cannot find user, returning HTTP 404 status")
274+
else:
275+
cla.log.debug(f"{fn} - loaded user {user.to_dict()} returning HTTP 200 status")
276+
return user.to_dict()
288277

289278
# Get session information for this request.
290279
cla.log.debug(f"{fn} - attempting to fetch OAuth2 token for state {state}")
@@ -1884,7 +1873,7 @@ def __init__(self, oauth2_token=False):
18841873
def _get_github_client(self, username, token):
18851874
return MockGitHubClient(username, token)
18861875

1887-
def _get_authorization_url_and_state(self, client_id, redirect_uri, scope, authorize_url):
1876+
def _get_authorization_url_and_state(self, client_id, redirect_uri, scope, authorize_url, state=None):
18881877
authorization_url = "http://authorization.url"
18891878
state = "random-state-here"
18901879
return authorization_url, state
@@ -1895,7 +1884,7 @@ def _fetch_token(self, client_id, state, token_url, client_secret, code): # pyl
18951884
def _get_request_session(self, request) -> dict:
18961885
if self.oauth2_token:
18971886
return {
1898-
"github_oauth2_token": "random-token", # LG: comment this out to see how Mock class woudl attempt to fetch GitHub token using state & code
1887+
"github_oauth2_token": "random-token", # LG: comment this out to see how Mock class would attempt to fetch GitHub token using state & code
18991888
"github_oauth2_state": "random-state",
19001889
"github_origin_url": "http://github/origin/url",
19011890
"github_installation_id": 1,

cla-backend/cla/routes.py

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1808,8 +1808,7 @@ def get_event(event_id: hug.types.text, response):
18081808
def user_from_session(request, response):
18091809
"""
18101810
GET: /user-from-session
1811-
Example: https://api.dev.lfcla.com/v2/user-from-session?redirect=0&redirect_url=localhost%3A4200
1812-
Example: https://api.dev.lfcla.com/v2/user-from-session?state=xyz&code=xyz
1811+
Example: https://api.dev.lfcla.com/v2/user-from-session
18131812
Returns user object from OAuth2 session
18141813
Example user returned:
18151814
{
@@ -1833,20 +1832,11 @@ def user_from_session(request, response):
18331832
"user_name": "Test User",
18341833
"version": "v1"
18351834
}
1836-
Can also return 302 redirect (if redirect mode is set redirect=1|yes|true)
1837-
Can also return 202 redirect_url for GitHub OAuth2 if redirect mode is not set (redirect=0|no|false) with payload:
1838-
{
1839-
"redirect_url": "https://github.com/login/oauth/authorize?response_type=code&client_id=38f6d46ff92b7ed04071&redirect_uri=abc&scope=user%3Aemail&state=VCshZQtMs0hPMw6XuMBBZODVaWAxXX"
1840-
}
1841-
Can also return 404 on OAuth2 errors or missing redirect_url when no session present
1842-
return_url should ideally be "CLA contributor console" URL + /v2/user-from-session, Github will add "?state=xyz&code=xyz"
1843-
"""
1844-
raw_redirect = request.params.get('redirect', 'false').lower()
1845-
redirect = raw_redirect in ('1', 'true', 'yes')
1846-
redirect_url = request.params.get('redirect_url', '')
1847-
state = request.params.get('state', '')
1848-
code = request.params.get('code', '')
1849-
return cla.controllers.repository_service.user_from_session(redirect, redirect_url, state, code, request, response)
1835+
Will 302 redirect to /v2/github/installation if there is no session and that callback will return user data then
1836+
Will return 200 and user data if there is an active GitHub session
1837+
Can return 404 on OAuth2 errors
1838+
"""
1839+
return cla.controllers.repository_service.user_from_session(request, response)
18501840

18511841

18521842
@hug.post("/events", versions=1)

cla-backend/cla/utils.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import json
1010
import os
1111
import re
12+
import secrets
13+
import base64
1214
import urllib.parse
1315
import urllib.parse as urlparse
1416
from datetime import datetime
@@ -1160,7 +1162,7 @@ def get_comment_body(repository_type, sign_url, signed: List[UserCommitSummary],
11601162
return text + committers_comment
11611163

11621164

1163-
def get_authorization_url_and_state(client_id, redirect_uri, scope, authorize_url):
1165+
def get_authorization_url_and_state(client_id, redirect_uri, scope, authorize_url, state=None):
11641166
"""
11651167
Helper function to get an OAuth2 session authorization URL and state.
11661168
@@ -1174,17 +1176,38 @@ def get_authorization_url_and_state(client_id, redirect_uri, scope, authorize_ur
11741176
:type authorize_url: string
11751177
"""
11761178
fn = "utils.get_authorization_url_and_state"
1177-
oauth = OAuth2Session(client_id, redirect_uri=redirect_uri, scope=scope)
1178-
authorization_url, state = oauth.authorization_url(authorize_url)
1179-
cla.log.debug(
1180-
f"{fn} - initialized a new oauth session "
1181-
f"using the github oauth client id: {client_id[0:5]}... "
1182-
f"with the redirect_uri: {redirect_uri} "
1183-
f"using scope of: {scope}. Obtained the "
1184-
f"state: {state} and the "
1185-
f"generated authorization_url: {authorize_url}"
1186-
)
1187-
return authorization_url, state
1179+
if state is None:
1180+
oauth = OAuth2Session(client_id, redirect_uri=redirect_uri, scope=scope)
1181+
authorization_url, state = oauth.authorization_url(authorize_url)
1182+
cla.log.debug(
1183+
f"{fn} - initialized a new oauth session "
1184+
f"using the github oauth client id: {client_id[0:5]}... "
1185+
f"with the redirect_uri: {redirect_uri} "
1186+
f"using scope of: {scope}. Obtained the "
1187+
f"state: {state} and the "
1188+
f"generated authorization_url: {authorize_url}"
1189+
)
1190+
return authorization_url, state
1191+
else:
1192+
csrf_token = secrets.token_urlsafe(16)
1193+
state_payload = {"csrf": csrf_token, "state": state }
1194+
state_json = json.dumps(state_payload)
1195+
encoded_state = base64.urlsafe_b64encode(state_json.encode()).decode()
1196+
oauth = OAuth2Session(client_id, redirect_uri=redirect_uri, scope=scope)
1197+
authorization_url, _ = oauth.authorization_url(authorize_url, state=encoded_state)
1198+
1199+
# Logging
1200+
cla.log.debug(
1201+
f"{fn} - initialized a new oauth session "
1202+
f"using the github oauth client id: {client_id[0:5]}... "
1203+
f"with the redirect_uri: {redirect_uri}. "
1204+
f"using scope of: {scope}. "
1205+
f"CSRF token: {csrf_token}. "
1206+
f"custom value: {state}. "
1207+
f"encoded state: {encoded_state}."
1208+
f"Generated authorization_url: {authorization_url}"
1209+
)
1210+
return authorization_url, csrf_token
11881211

11891212

11901213
def fetch_token(client_id, state, token_url, client_secret, code, redirect_uri=None): # pylint: disable=too-many-arguments

utils/api_urls.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# V1/V2
2+
API_URL="https://api.easycla.lfx.linuxfoundation.org"
3+
API_URL="https://api.lfcla.dev.platform.linuxfoundation.org"
4+
5+
# V3/V4
6+
API_URL="https://api-gw.platform.linuxfoundation.org/cla-service"
7+
API_URL="https://api-gw.dev.platform.linuxfoundation.org/cla-service"

utils/get_user_from_session_py.sh

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,17 @@
11
#!/bin/bash
22
# API_URL=https://[xyz].ngrok-free.app (defaults to localhost:5000)
33
# API_URL=https://api.lfcla.dev.platform.linuxfoundation.org
4-
# REDIRECT=0|1 DEBUG='' ./utils/get_user_from_session_py.sh
5-
# API_URL=https://api.lfcla.dev.platform.linuxfoundation.org DEBUG=1 REDIRECT=0 ./utils/get_user_from_session_py.sh 'https://contributor.easycla.lfx.linuxfoundation.org'
6-
# CODE=xyz STAE=xyz
7-
# ./utils/ngrok.sh then DEBUG='' REDIRECT=0 ./utils/get_user_from_session_py.sh 'https://edc5-147-75-85-27.ngrok-free.app/v2/user-from-session'
8-
# yarn serve:ext:
9-
# DEBUG='' REDIRECT=0 ./utils/get_user_from_session_py.sh 'http://147.75.85.27:5000/v2/user-from-session'
10-
# yarn serve:ext && API_URL='http://147.75.85.27:5000' DEBUG='' REDIRECT=0 ./utils/get_user_from_session_py.sh 'http://147.75.85.27:5000/v2/user-from-session'
11-
12-
export redirect_url="${1}"
13-
export encoded_redirect_url=$(jq -rn --arg x "$redirect_url" '$x|@uri')
4+
# DEBUG='' ./utils/get_user_from_session_py.sh
5+
# Flow with custom GitHub app: see 'LG:' in cla/controllers/repository_service.py, then:
6+
# Start server via: CLA_API_BASE_CLI='http://147.75.85.27:5000' GH_OAUTH_CLIENT_ID_CLI="$(cat ../lg-github-oauth-app.client-id.secret)" GH_OAUTH_SECRET_CLI="$(cat ../lg-github-oauth-app.client-secret.secret)" yarn serve:ext
7+
# In the browser: open page: http://147.75.85.27:5000/v2/user-from-session
148

159
if [ -z "$API_URL" ]
1610
then
1711
export API_URL="http://localhost:5000"
1812
fi
1913

20-
if [ -z "${REDIRECT}" ]
21-
then
22-
export REDIRECT="0"
23-
fi
24-
25-
if ( [ -z "${CODE}" ] && [ -z "${STATE}" ] )
26-
then
27-
export API="${API_URL}/v2/user-from-session?redirect=${REDIRECT}&redirect_url=${encoded_redirect_url}"
28-
else
29-
export API="${API_URL}/v2/user-from-session?code=${CODE}&state=${STATE}"
30-
fi
14+
export API="${API_URL}/v2/user-from-session"
3115

3216
if [ ! -z "$DEBUG" ]
3317
then

0 commit comments

Comments
 (0)