Skip to content

Commit

Permalink
Update auth mechanism to use Google
Browse files Browse the repository at this point in the history
* Pod Point has changed their auth mechanism to Google. Adds support
* Pod Point has changed their API base. Adds support
  • Loading branch information
mattrayner committed Feb 6, 2024
1 parent 32f6c6a commit 9d32bc5
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 177 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Pod Point Client Changelog

## v1.4.0

* Update auth system to new Google-based auth from Pod Point

## v1.3.1

* Add `pytz` as a dependency
Expand Down
9 changes: 8 additions & 1 deletion podpointclient/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
CHARGE_OVERRIDE = '/charge-override'
FIRMWARE = '/firmware'

API_BASE = 'api.pod-point.com'
API_BASE = 'mobile-api.pod-point.com/api3/'
API_VERSION = 'v5'
API_BASE_URL = 'https://' + API_BASE + '/' + API_VERSION

"""Google endpoint, used for auth"""
GOOGLE_KEY = '?key=AIzaSyCwhF8IOl_7qHXML0pOd5HmziYP46IZAGU'
PASSWORD_VERIFY = '/verifyPassword' + GOOGLE_KEY

GOOGLE_BASE = 'www.googleapis.com/identitytoolkit/v3/relyingparty'
GOOGLE_BASE_URL = 'https://' + GOOGLE_BASE
31 changes: 18 additions & 13 deletions podpointclient/helpers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from ..errors import APIError, AuthError, SessionError
from .session import Session
from ..endpoints import API_BASE_URL, AUTH
from ..endpoints import GOOGLE_BASE_URL, PASSWORD_VERIFY
from .functions import HEADERS
from .api_wrapper import APIWrapper

Expand All @@ -25,6 +25,7 @@ def __init__(
self.email: str = email
self.password: str = password
self.access_token: str = None
self.refresh_token: str = None
self.access_token_expiry: datetime = None
self._session: aiohttp.ClientSession = session
self._api_wrapper: APIWrapper = APIWrapper(session=self._session)
Expand All @@ -39,22 +40,25 @@ def user_id(self):

def check_access_token(self) -> bool:
"""Does the current access token need refreshing?"""
access_token_set: bool = (
self.access_token is not None and self.access_token_expiry is not None
)
access_token_not_expired: bool = (
access_token_set and datetime.now() < self.access_token_expiry
access_token_not_expired: bool = self.access_token_expired() is False

return bool(self.access_token_set() and access_token_not_expired)

def access_token_set(self) -> bool:
return (
self.access_token is not None and self.access_token_expiry is not None
)

return bool(access_token_set and access_token_not_expired)
def access_token_expired(self) -> bool:
"""Is the current access token expired"""
return self.access_token_set() and datetime.now() > self.access_token_expiry

async def async_update_access_token(self) -> bool:
"""Update access token, if needed."""
if self.check_access_token():
return True

try:
session_created: bool = False
_LOGGER.debug('Updating access token')
access_token_updated: bool = await self.__update_access_token()

Expand Down Expand Up @@ -83,24 +87,25 @@ async def async_update_access_token(self) -> bool:
_LOGGER.error("Error creating session. %s", exception)
raise exception

async def __update_access_token(self) -> bool:
async def __update_access_token(self, refresh: bool = False) -> bool:
return_value = False

try:
wrapper = APIWrapper(session=self._session)
response = await wrapper.post(
url=f"{API_BASE_URL}{AUTH}",
body={"username": self.email, "password": self.password},
url=f"{GOOGLE_BASE_URL}{PASSWORD_VERIFY}",
body={"username": self.email, "returnSecureToken": True, "password": self.password},
headers=HEADERS,
exception_class=AuthError)

if response.status != 200:
await self.__handle_response_error(response, AuthError)

json = await response.json()
self.access_token = json["access_token"]
self.access_token = json["idToken"]
self.refresh_token = json["refreshToken"]
self.access_token_expiry = datetime.now() + timedelta(
seconds=json["expires_in"] - 10
seconds=json["expiresIn"] - 10
)
return_value = True

Expand Down
2 changes: 1 addition & 1 deletion podpointclient/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version for the podpointclient library"""

__version__ = "1.3.1"
__version__ = "1.4.0"
3 changes: 2 additions & 1 deletion requirements_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ pytz==2022.1
pytest-aiohttp
pytest-asyncio
pytest-cov
pytest-timeout
pytest-timeout
async-timeout
7 changes: 3 additions & 4 deletions tests/fixtures/auth.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"token_type": "Bearer",
"expires_in": 1234,
"access_token": "1234",
"refresh_token": "1234"
"expiresIn": 1234,
"idToken": "1234",
"refreshToken": "1234"
}
4 changes: 2 additions & 2 deletions tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import os

from podpointclient.endpoints import API_BASE_URL, AUTH, CHARGE_SCHEDULES, PODS, SESSIONS, UNITS, USERS, CHARGES, FIRMWARE
from podpointclient.endpoints import GOOGLE_BASE_URL, PASSWORD_VERIFY, API_BASE_URL, AUTH, CHARGE_SCHEDULES, PODS, SESSIONS, UNITS, USERS, CHARGES, FIRMWARE

class Mocks:
def __init__(self, m = None) -> None:
Expand All @@ -24,7 +24,7 @@ def happy_path(self, include_timestamp=False):
and_timestamp = f'&{timestamp}'
question_timestamp = f'?{timestamp}'

self.m.post(f'{API_BASE_URL}{AUTH}', payload=auth_response)
self.m.post(f'{GOOGLE_BASE_URL}{PASSWORD_VERIFY}', payload=auth_response)
self.m.post(f'{API_BASE_URL}{SESSIONS}', payload=session_response)
self.m.get(f'{API_BASE_URL}{USERS}/1234{PODS}?perpage=1&page=1{and_timestamp}', payload=pods_response)
self.m.get(f'{API_BASE_URL}{USERS}/1234{PODS}?perpage=5&page=1&include=statuses,price,model,unit_connectors,charge_schedules,charge_override{and_timestamp}', payload=pods_response)
Expand Down
78 changes: 35 additions & 43 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from aioresponses import aioresponses
import logging

from podpointclient.endpoints import API_BASE, API_BASE_URL, API_VERSION, AUTH, SESSIONS
from podpointclient.endpoints import GOOGLE_BASE_URL, PASSWORD_VERIFY, API_BASE_URL, API_VERSION, AUTH, SESSIONS

import pytest

Expand Down Expand Up @@ -62,10 +62,9 @@ async def test_access_token_not_set():
@pytest.mark.asyncio
async def test_update_access_token_when_not_set(aiohttp_client):
auth_response = {
"token_type": "Bearer",
"expires_in": 1234,
"access_token": "1234",
"refresh_token": "1234"
"expiresIn": 1234,
"idToken": "1234",
"refreshToken": "1234"
}
session_response = {
"sessions": {
Expand All @@ -75,7 +74,7 @@ async def test_update_access_token_when_not_set(aiohttp_client):
}

with aioresponses() as m:
m.post(f'{API_BASE_URL}{AUTH}', payload=auth_response)
m.post(f'{GOOGLE_BASE_URL}{PASSWORD_VERIFY}', payload=auth_response)
m.post(f'{API_BASE_URL}{SESSIONS}', payload=session_response)

async with aiohttp.ClientSession() as session:
Expand All @@ -92,10 +91,9 @@ async def test_update_access_token_when_not_set(aiohttp_client):
@pytest.mark.asyncio
async def test_auth_with_session_error(aiohttp_client):
auth_response = {
"token_type": "Bearer",
"expires_in": 1234,
"access_token": "1234",
"refresh_token": "1234"
"expiresIn": 1234,
"idToken": "1234",
"refreshToken": "1234"
}
session_response = {
"foo": {
Expand All @@ -105,7 +103,7 @@ async def test_auth_with_session_error(aiohttp_client):
}

with aioresponses() as m:
m.post(f'{API_BASE_URL}{AUTH}', payload=auth_response)
m.post(f'{GOOGLE_BASE_URL}{PASSWORD_VERIFY}', payload=auth_response)
m.post(f'{API_BASE_URL}{SESSIONS}', payload=session_response)

async with aiohttp.ClientSession() as session:
Expand All @@ -120,10 +118,9 @@ async def test_auth_with_session_error(aiohttp_client):
@pytest.mark.asyncio
async def test_auth_with_auth_error(aiohttp_client):
auth_response = {
"token_type": "Bearer",
"expires_in": 1234,
"access_token": "1234",
"refresh_token": "1234"
"expiresIn": 1234,
"idToken": "1234",
"refreshToken": "1234"
}
session_response = {
"session": {
Expand All @@ -133,7 +130,7 @@ async def test_auth_with_auth_error(aiohttp_client):
}

with aioresponses() as m:
m.post(f'{API_BASE_URL}{AUTH}', status=201, payload=auth_response)
m.post(f'{GOOGLE_BASE_URL}{PASSWORD_VERIFY}', status=201, payload=auth_response)

async with aiohttp.ClientSession() as session:
auth = subject(session)
Expand All @@ -148,10 +145,9 @@ async def test_auth_with_auth_error(aiohttp_client):
@pytest.mark.asyncio
async def test_update_access_token_when_not_set(aiohttp_client):
auth_response = {
"token_type": "Bearer",
"expires_in": 1234,
"access_token": "1234",
"refresh_token": "1234"
"expiresIn": 1234,
"idToken": "1234",
"refreshToken": "1234"
}
session_response = {
"sessions": {
Expand All @@ -161,14 +157,14 @@ async def test_update_access_token_when_not_set(aiohttp_client):
}

with aioresponses() as m:
m.post(f'{API_BASE_URL}{AUTH}', payload=auth_response)
m.post(f'{GOOGLE_BASE_URL}{PASSWORD_VERIFY}', payload=auth_response)
m.post(f'{API_BASE_URL}{SESSIONS}', payload=session_response)

async with aiohttp.ClientSession() as session:
auth = expired_subject(session)
assert auth.access_token_expiry < datetime.now()

m.post(f'{API_BASE_URL}{AUTH}', payload=auth_response)
m.post(f'{GOOGLE_BASE_URL}{PASSWORD_VERIFY}', payload=auth_response)
m.post(f'{API_BASE_URL}{SESSIONS}', payload=session_response)

result2 = await auth.async_update_access_token()
Expand All @@ -183,7 +179,7 @@ async def test_update_access_token_when_token_valid():

async def test_auth_401_error():
with aioresponses() as m:
m.post(f'{API_BASE_URL}{AUTH}', status=401 , body="foo error")
m.post(f'{GOOGLE_BASE_URL}{PASSWORD_VERIFY}', status=401 , body="foo error")

async with aiohttp.ClientSession() as session:
auth = expired_subject(session)
Expand All @@ -196,32 +192,30 @@ async def test_auth_401_error():
async def test_auth_json_error():
# MISSING ELEMENT
auth_response = {
"token_type": "Bearer",
"expires_in": "a74f3",
"refresh_token": "1234"
"expiresIn": 1234,
"refreshToken": "1234"
}

with aioresponses() as m:
m.post(f'{API_BASE_URL}{AUTH}', payload=auth_response)
m.post(f'{GOOGLE_BASE_URL}{PASSWORD_VERIFY}', payload=auth_response)

async with aiohttp.ClientSession() as session:
auth = expired_subject(session)

with pytest.raises(AuthError) as exc_info:
await auth.async_update_access_token()

assert "Auth Error (200) - Error processing access token response. 'access_token' not found in json." in str(exc_info.value)
assert "Auth Error (200) - Error processing access token response. 'idToken' not found in json." in str(exc_info.value)

# INVALID EXPIRES_IN
auth_response = {
"token_type": "Bearer",
"expires_in": "F14A3",
"access_token": "1234",
"refresh_token": "1234"
"expiresIn": "F14A3",
"idToken": "1234",
"refreshToken": "1234"
}

with aioresponses() as m:
m.post(f'{API_BASE_URL}{AUTH}', payload=auth_response)
m.post(f'{GOOGLE_BASE_URL}{PASSWORD_VERIFY}', payload=auth_response)

async with aiohttp.ClientSession() as session:
auth = expired_subject(session)
Expand All @@ -234,14 +228,13 @@ async def test_auth_json_error():

async def test_session_401_error():
auth_response = {
"token_type": "Bearer",
"expires_in": 1234,
"access_token": "1234",
"refresh_token": "1234"
"expiresIn": 1234,
"idToken": "1234",
"refreshToken": "1234"
}

with aioresponses() as m:
m.post(f'{API_BASE_URL}{AUTH}', payload=auth_response)
m.post(f'{GOOGLE_BASE_URL}{PASSWORD_VERIFY}', payload=auth_response)
m.post(f'{API_BASE_URL}{SESSIONS}', status=401, body="bar error")

async with aiohttp.ClientSession() as session:
Expand All @@ -254,10 +247,9 @@ async def test_session_401_error():

async def test_session_json_error():
auth_response = {
"token_type": "Bearer",
"expires_in": 1234,
"access_token": "1234",
"refresh_token": "1234"
"idToken": "1234",
"expiresIn": 1234,
"refreshToken": "1234"
}
session_response = {
"sessions": {
Expand All @@ -266,7 +258,7 @@ async def test_session_json_error():
}

with aioresponses() as m:
m.post(f'{API_BASE_URL}{AUTH}', payload=auth_response)
m.post(f'{GOOGLE_BASE_URL}{PASSWORD_VERIFY}', payload=auth_response)
m.post(f'{API_BASE_URL}{SESSIONS}', payload=session_response)

async with aiohttp.ClientSession() as session:
Expand Down
Loading

0 comments on commit 9d32bc5

Please sign in to comment.