Source code for sni.esi.sso

"""
This module handles the communication with EVE SSO
"""

from base64 import urlsafe_b64encode
import logging
from typing import List
from urllib.parse import urljoin

from pydantic.error_wrappers import ValidationError
from requests.exceptions import HTTPError
import jwt
import pydantic as pdt
import requests

from sni.conf import CONFIGURATION as conf

from .scope import EsiScope


[docs]class AuthorizationCodeResponse(pdt.BaseModel): """ A token document issued by the ESI looks like this:: { "access_token": "jZOzkRtA8B...LQJg2", "token_type": "Bearer", "expires_in": 1199, "refresh_token": "RGuc...w1" } """ access_token: str token_type: str expires_in: int refresh_token: str
[docs]class DecodedAccessToken(pdt.BaseModel): """ Decoded access token issued by the ESI. Should look like this:: { "scp": [ "esi-skills.read_skills.v1", "esi-skills.read_skillqueue.v1" ], "jti": "998e12c7-3241-43c5-8355-2c48822e0a1b", "kid": "JWT-Signature-Key", "sub": "CHARACTER:EVE:123123", "azp": "my3rdpartyclientid", "name": "Some Bloke", "owner": "8PmzCeTKb4VFUDrHLc/AeZXDSWM=", "exp": 1534412504, "iss": "login.eveonline.com" } """ azp: str exp: int iss: str jti: str kid: str name: str owner: str scp: List[EsiScope] sub: str @property def character_id(self) -> int: """ Extracts the character ID from the ``sub`` field. """ prefix = "CHARACTER:EVE:" if not self.sub.startswith(prefix): raise ValueError('Unexpected "sub" field format: ' + self.sub) return int(self.sub[len(prefix) :])
[docs]class EsiTokenError(Exception): """ Raised when something goes wrong with tokens. """
[docs]def decode_access_token(access_token: str) -> DecodedAccessToken: """ Converts an access token in JWT form to a :class:`sni.esi.sso.DecodedAccessToken` """ document = jwt.decode(access_token, verify=False) if isinstance(document["scp"], str): document["scp"] = [document["scp"]] try: return DecodedAccessToken(**document) except ValidationError as error: logging.error("Failed to decode access token: %s", str(error)) raise EsiTokenError
[docs]def get_auth_url( esi_scopes: List[EsiScope], state: str = "00000000-0000-0000-0000-000000000000", ) -> str: """ Returns an EVE SSO login url based on the scopes passed in argument. Args: esi_scopes: List of esi scope state: A state string (default: ``None``) Returns: A url in string form """ if not esi_scopes: logging.warning( "Scope list cannot be empty, replaced by ['publicData']" ) esi_scopes = [EsiScope.PUBLICDATA] params = { "client_id": conf.esi.client_id.get_secret_value(), "redirect_uri": urljoin(conf.general.root_url, "/callback/esi"), "response_type": "code", "scope": " ".join(esi_scopes), "state": state, } request = requests.Request( "GET", "https://login.eveonline.com/v2/oauth/authorize/", params=params, ) url = request.prepare().url if not url: raise RuntimeError("Could not construct a valid auth URL") return url
[docs]def get_basic_authorization_code() -> str: """ Returns an authorization code derived from the ESI ``client_id`` and ``client_secret``. More precisely, it is:: urlsafe_b64encode('<client_id>:<client_secret>') """ authorization = ( str(conf.esi.client_id.get_secret_value()) + ":" + str(conf.esi.client_secret.get_secret_value()) ) return urlsafe_b64encode(authorization.encode()).decode()
[docs]def get_access_token_from_callback_code( code: str, ) -> AuthorizationCodeResponse: """ Gets an access token (along with its refresh token) from an EVE SSO authorization code. See also: :class:`sni.esi.sso.AuthorizationCodeResponse` Returns: The document issued by the ESI, or ``None`` Reference: `OAuth 2.0 for Web Based Applications <https://docs.esi.evetech.net/docs/sso/web_based_sso_flow.html>` Todo: Validate the ESI JWT token """ data = { "code": code, "grant_type": "authorization_code", } headers = { "Authorization": "Basic " + get_basic_authorization_code(), "Content-Type": "application/x-www-form-urlencoded", "Host": "login.eveonline.com", } response = requests.post( "https://login.eveonline.com/v2/oauth/token", headers=headers, data=data, ) try: response.raise_for_status() return AuthorizationCodeResponse(**response.json()) except HTTPError as error: logging.error("Failed to get access token: %s", str(error)) except ValidationError as error: logging.error( "Failed to parse authorization code response: %s", str(error) ) raise EsiTokenError
[docs]def refresh_access_token(refresh_token: str) -> AuthorizationCodeResponse: """ Refreshes an access token. Returns: The response in an :class:`sni.esi.sso.AuthorizationCodeResponse` Reference: `esi-docs <https://docs.esi.evetech.net/docs/sso/refreshing_access_tokens.html>`_ """ data = { "grant_type": "refresh_token", "refresh_token": refresh_token, } headers = { "Content-Type": "application/x-www-form-urlencoded", "Host": "login.eveonline.com", "Authorization": "Basic " + get_basic_authorization_code(), } response = requests.post( "https://login.eveonline.com/v2/oauth/token", headers=headers, data=data, ) try: response.raise_for_status() return AuthorizationCodeResponse(**response.json()) except HTTPError as error: logging.error("Failed to refresh access token: %s", str(error)) except ValidationError as error: logging.error( "Failed to parse authorization code response: %s", str(error) ) raise EsiTokenError