Token validation and various cleanup

This commit is contained in:
blitzmann
2021-10-17 21:01:30 -04:00
parent 1874cbe0c5
commit 33aa208513
4 changed files with 125 additions and 114 deletions

View File

@@ -11,3 +11,4 @@ packaging >= 16.8
roman >= 2.0.0
beautifulsoup4 >= 4.6.0
pyyaml >= 5.1
python-jose==3.0.1

View File

@@ -14,7 +14,7 @@ from eos.saveddata.ssocharacter import SsoCharacter
from service.esiAccess import APIException
import gui.globalEvents as GE
from gui.ssoLogin import SsoLogin, SsoLoginServer
from service.server import StoppableHTTPServer, AuthHandler
from service.server import StoppableHTTPServer, AuthHandler, SSOError
from service.settings import EsiSettings
from service.esiAccess import EsiAccess
import gui.mainFrame
@@ -134,14 +134,7 @@ class Esi(EsiAccess):
return 'http://localhost:{}'.format(port)
def handleLogin(self, message):
# we already have authenticated stuff for the auto mode
if self.settings.get('ssoMode') == EsiSsoMode.AUTO:
ssoInfo = message['SSOInfo'][0]
auth_response = json.loads(base64.b64decode(ssoInfo))
else:
# otherwise, we need to fetch the information
auth_response = self.auth(message['code'][0])
auth_response = self.auth(message['code'][0])
res = self._session.get(
self.oauth_verify,
@@ -169,11 +162,17 @@ class Esi(EsiAccess):
def handleServerLogin(self, message):
if not message:
raise Exception("Could not parse out querystring parameters.")
raise SSOError("Could not parse out querystring parameters.")
if message['state'][0] != self.state:
try:
state_enc = message['state'][0]
state = json.loads(base64.b64decode(state_enc))['state']
except Exception:
raise SSOError("There was a problem decoding state parameter.")
if state != self.state:
pyfalog.warn("OAUTH state mismatch")
raise Exception("OAUTH State Mismatch.")
raise SSOError("OAUTH State Mismatch.")
pyfalog.debug("Handling SSO login with: {0}", message)

View File

@@ -18,9 +18,12 @@ import base64
import secrets
import hashlib
import json
from jose import jwt
from jose.exceptions import ExpiredSignatureError, JWTError, JWTClaimsError
import datetime
from service.const import EsiSsoMode, EsiEndpoints
from service.server import SSOError
from service.settings import EsiSettings, NetworkSettings
from requests import Session
@@ -131,45 +134,37 @@ class EsiAccess:
def getLoginURI(self, redirect=None):
self.state = str(uuid.uuid4())
if self.settings.get("ssoMode") == EsiSsoMode.AUTO:
# Generate the PKCE code challenge
self.code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32))
m = hashlib.sha256()
m.update(self.code_verifier)
d = m.digest()
code_challenge = base64.urlsafe_b64encode(d).decode().replace("=", "")
# Generate the PKCE code challenge
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32))
m = hashlib.sha256()
m.update(code_verifier)
d = m.digest()
code_challenge = base64.urlsafe_b64encode(d).decode().replace("=", "")
state_arg = {
'mode': self.settings.get('loginMode'),
'redirect': redirect,
'state': self.state
}
args = {
# 'pyfa_version': config.version,
# 'login_method': self.settings.get('loginMode'), # todo: encode this into the state
# 'client_hash': config.getClientSecret(),
'response_type': 'code',
'redirect_uri': 'http://127.0.0.1:5500/callback.html',
'client_id': '095d8cd841ac40b581330919b49fe746', # pyfa PKCE app # TODO: move this to some central config location, not hardcoded
'scope': ' '.join(scopes),
'code_challenge': code_challenge,
'code_challenge_method': 'S256',
'state': base64.b64encode(bytes(json.dumps(state_arg), 'utf-8'))
}
state_arg = {
'mode': self.settings.get('loginMode'),
'redirect': redirect,
'state': self.state
}
args = {
# 'pyfa_version': config.version,
# 'login_method': self.settings.get('loginMode'), # todo: encode this into the state
# 'client_hash': config.getClientSecret(),
'response_type': 'code',
'redirect_uri': 'http://127.0.0.1:5500/callback.html',
'client_id': self.settings.get('clientID') or '095d8cd841ac40b581330919b49fe746', # pyfa PKCE app # TODO: move this to some central config location, not hardcoded
'scope': ' '.join(scopes),
'code_challenge': code_challenge,
'code_challenge_method': 'S256',
'state': base64.b64encode(bytes(json.dumps(state_arg), 'utf-8'))
}
return '%s?%s' % (
self.oauth_authorize,
urlencode(args)
)
return '%s?%s' % (
self.oauth_authorize,
urlencode(args)
)
else:
return '%s?response_type=%s&redirect_uri=%s&client_id=%s%s%s' % (
self.oauth_authorize,
'code',
quote('http://localhost:6461', safe=''),
self.settings.get('clientID'),
'&scope=%s' % '+'.join(scopes) if scopes else '',
'&state=%s' % self.state
)
def get_oauth_header(self, token):
""" Return the Bearer Authorization header required in oauth calls
@@ -229,21 +224,83 @@ class EsiAccess:
{
'grant_type': 'authorization_code',
'code': code,
'client_id': self.settings.get('clientID') or '095d8cd841ac40b581330919b49fe746',
"code_verifier": self.code_verifier
}
)
def auth(self, code):
request_data = self.get_access_token_request_params(code)
res = self._session.post(**request_data)
# todo: handle invalid auth code, or one that has been used already
values = {
'grant_type': 'authorization_code',
'code': code,
'client_id': self.settings.get('clientID') or '095d8cd841ac40b581330919b49fe746',
"code_verifier": self.code_verifier
}
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Host": "login.eveonline.com",
}
res = self._session.post(
"https://login.eveonline.com/v2/oauth/token",
data=values,
headers=headers,
)
if res.status_code != 200:
raise Exception(
request_data['url'],
raise SSOError(
"https://login.eveonline.com/v2/oauth/token",
res.status_code,
res.json()
)
json_res = res.json()
self.validate_eve_jwt(json_res['access_token'])
return json_res
def validate_eve_jwt(self, jwt_token):
"""Validate a JWT token retrieved from the EVE SSO.
Args:
jwt_token: A JWT token originating from the EVE SSO
Returns
dict: The contents of the validated JWT token if there are no
validation errors
"""
jwk_set_url = "https://login.eveonline.com/oauth/jwks"
res = self._session.get(jwk_set_url)
res.raise_for_status()
data = res.json()
try:
jwk_sets = data["keys"]
except KeyError as e:
raise SSOError("Something went wrong when retrieving the JWK set. The returned "
"payload did not have the expected key {}. \nPayload returned "
"from the SSO looks like: {}".format(e, data))
jwk_set = next((item for item in jwk_sets if item["alg"] == "RS256"))
try:
return jwt.decode(
jwt_token,
jwk_set,
algorithms=jwk_set["alg"],
issuer=["login.eveonline.com", "https://login.eveonline.com"]
)
except ExpiredSignatureError as e:
raise SSOError("The JWT token has expired: {}").format(str(e))
except JWTError as e:
raise SSOError("The JWT signature was invalid: {}").format(str(e))
except JWTClaimsError as e:
raise SSOError("The issuer claim was not from login.eveonline.com or "
"https://login.eveonline.com: {}".format(str(e)))
def refresh(self, ssoChar):
request_data = self.get_refresh_token_params(config.cipher.decrypt(ssoChar.refreshToken).decode())
res = self._session.post(**request_data)

View File

@@ -5,67 +5,11 @@ import threading
from logbook import Logger
import socketserver
import json
pyfalog = Logger(__name__)
# noinspection PyPep8
HTML = '''
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<title>pyfa Local Server</title>
<style type="text/css">
body {{ text-align: center; padding: 150px; }}
h1 {{ font-size: 40px; }}
h2 {{ font-size: 32px; }}
body {{ font: 20px Helvetica, sans-serif; color: #333; }}
#article {{ display: block; text-align: left; width: 650px; margin: 0 auto; }}
a {{ color: #dc8100; text-decoration: none; }}
a:hover {{ color: #333; text-decoration: none; }}
</style>
</head>
<body>
<!-- Layout from Short Circuit's CREST login. Shout out! https://github.com/farshield/shortcircuit -->
<div id="article">
<h1>pyfa</h1>
{0}
</div>
<script type="text/javascript">
function extractFromHash(name, hash) {{
var match = hash.match(new RegExp(name + "=([^&]+)"));
return !!match && match[1];
}}
var hash = window.location.hash;
var token = extractFromHash("access_token", hash);
var step2 = extractFromHash("step2", hash);
function doRedirect() {{
if (token){{
// implicit authentication
var redirect = window.location.origin.concat('/?', window.location.hash.substr(1), '&step=2');
window.location = redirect;
}}
else {{
// user-defined
var redirect = window.location.href + '&step=2';
window.location = redirect;
}}
}}
// do redirect if we are not already on step 2
if (window.location.href.indexOf('step=2') == -1) {{
setTimeout(doRedirect(), 1000);
}}
</script>
</body>
</html>
'''
class SSOError(Exception):
pass
# https://github.com/fuzzysteve/CREST-Market-Downloader/
class AuthHandler(http.server.BaseHTTPRequestHandler):
@@ -86,16 +30,26 @@ class AuthHandler(http.server.BaseHTTPRequestHandler):
pyfalog.info("Successfully logged into EVE.")
is_success = True
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
except (KeyboardInterrupt, SystemExit):
raise
except SSOError as ex:
pyfalog.error("Error logging into EVE")
pyfalog.error(ex)
self.send_response(500)
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(str.encode(str(ex)))
except Exception as ex:
pyfalog.error("Error logging into EVE")
pyfalog.error(ex)
self.send_response(500)
# send error
finally:
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
# send error
if is_success:
self.server.stop()