Is it possible to convert HTTPBearer.credentials to Pydantic BaseModel in FastAPI? - google-oauth

I'm using FastAPI's HTTPBearer class to receive authorization tokens on request headers.
This is my dependencies file as per the FastAPI docs, which is used to retrieve the token on any request to the API.
classroom_auth_scheme = HTTPBearer(
scheme_name="Google Auth Credentials",
bearerFormat="Bearer",
description="O-Auth2 Credentials obtained on frontend, used to authenticate with Google services",
)
def get_classroom_token(
token: str = Depends(classroom_auth_scheme),
) -> requests.ClassroomAuthCredentials:
"""Converts a json string of Authorization Bearer token into ClassroomAuthCredentials class
Args:
token (str, optional): Autorization Header Bearer Token. Defaults to Depends(auth_scheme).
Raises:
HTTPException: 400 level response meaning the token was not in the correct format
Returns:
requests.ClassroomAuthCredentials
"""
try:
# token.credentials is a JSON String -> want: pydantic Basemodel
token_dict = json.loads(token.credentials)
token = requests.ClassroomAuthCredentials.parse_obj(token_dict)
return token
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"{e}",
)
And this is how I receive the token in my routes:
#router.post("/test-auth", summary="Validate authentication with Google Classroom API")
async def test_auth(token=Depends(get_classroom_token)):
try:
gc_service_test = get_service(token)
gc_api_test = ClassroomApi(service=gc_service_test)
user_profile = gc_api_test.get_user_profile("me")
response: responses.ListGoogleClassroomCourses = {
"message": f"Auth Credentials Are Valid",
"userProfile": user_profile,
}
return JSONResponse(response)
except errors.HttpError as error:
# handle exceptions...
Is there a way to specify the data structure of token.credentials like there is for request body?
This would make it easier to access properties as well as provide a format in the authorize modal on the swagger docs for other developers on the team, so they don't have to guess what the required properties of the Authorization token are.
This is my data model of the token.credentials as a BaseModel
class ClassroomAuthCredentials(BaseModel):
token: str = Field(..., example="MyJWT")
clientId: str = Field(..., example="myClientId")
clientSecret: str = Field(..., example="myClientSecret")
refreshToken: str = Field(..., example="myRefreshToken")
scopes: list[str] = Field(
...,
example=[
"https://www.googleapis.com/auth/classroom.courses.readonly",
"https://www.googleapis.com/auth/classroom.coursework.students",
],
)

Related

OAuth2: Unable to Authenticate API request

Been tasked to export forms and items from Podio using the API. Trying to do this with straight Python and Requests instead of the canned API tool. Am successful at retrieving the access and refresh tokens, but am unable to make the simplest Get request. The response has the error:
"error_description":"Authentication as None is not allowed for this method"
Tried this with 2 versions of using OAuth2 in Requests, both return that response.
What is it trying to tell me? Aside from giving the token, is there any other authentication attributes required?
client = BackendApplicationClient(client_id=CLIENT_ID)
oauth = OAuth2Session(client=client)
token = oauth.fetch_token(token_url=auth_url, client_id=CLIENT_ID,
client_secret=CLIENT_SECRET)
print('token:', token)
access_token = token["access_token"]
api_url = base_url + 'user/status'
r = oauth.get(api_url)
print(r.text)
headers = {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
"Authorization": "Bearer " + token["access_token"]}
response = requests.get(api_url, headers=headers, verify=True)
print(response.text)
Here is full response:
{"error_parameters":{},"error_detail":null,"error_propagate":false,"request":{"url":"http://api.podio.com/user/status","query_string":"","method":"GET"},"error_description":"Authentication as None is not allowed for this method","error":"forbidden"}

Cannot POST request using service account key file in Python, getting 'Invalid IAP credentials: Unable to parse JWT', '401 Status Code'

I am trying to send a POST request to a Google App Engine service with a JSON body accompanied by an authorization token. I am generating the access token from a local service account key JSON file. The code below is generating a credential but finally the authorization is being rejected. I also tried different ways already. Even tried writing the request in Postman with a Bearer token in the Header, or even as a plain cURL command. But whatever I try, getting a 401 authentication error. I need to make sure whether the problem is in my side or on the other side with the service. Explored every documentation avaliable but no luck.
from google.auth.transport import requests
from google.oauth2 import service_account
from google.auth.transport.requests import AuthorizedSession
CREDENTIAL_SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
CREDENTIALS_KEY_PATH = 'my-local-service-account-key-file.json'
#the example service url I am trying to hit with requests
url = 'https://test.appspot.com/submit'
headers = {"Content-Type": "application/json"}
#example data I am sending with the request body
payload = {
"key1": "value 1",
"key2": "value 2"
}
credentials = service_account.Credentials.from_service_account_file(
CREDENTIALS_KEY_PATH,
scopes=CREDENTIAL_SCOPES
)
credentials.refresh(requests.Request())
authed_session = AuthorizedSession(credentials)
response = authed_session.request('POST',
url,
headers=headers,
data=payload
)
#adding some debug lines for your help
print(response.text)
print(response.status_code)
print(response.headers)
Getting the Output:
Invalid IAP credentials: Unable to parse JWT
401
{'X-Goog-IAP-Generated-Response': 'true', 'Date': 'Mon, 03 May 2021 06:52:11 GMT', 'Content-Type': 'text/html', 'Server': 'Google Frontend', 'Content-Length': '44', 'Alt-Svc': 'h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"'}
IAP expects a JWT(OpenID Connect (OIDC)) token in the Authorization header while your method will attach an access token the the Authorization header instead. Take a look at the below code snippet to make a request to an IAP secured resource.
Your code needs to be something like the following:
from google.auth.transport.requests import Request
from google.oauth2 import id_token
import requests
def make_iap_request(url, client_id, method='GET', **kwargs):
"""Makes a request to an application protected by Identity-Aware Proxy.
Args:
url: The Identity-Aware Proxy-protected URL to fetch.
client_id: The client ID used by Identity-Aware Proxy.
method: The request method to use
('GET', 'OPTIONS', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE')
**kwargs: Any of the parameters defined for the request function:
https://github.com/requests/requests/blob/master/requests/api.py
If no timeout is provided, it is set to 90 by default.
Returns:
The page body, or raises an exception if the page couldn't be retrieved.
"""
# Set the default timeout, if missing
if 'timeout' not in kwargs:
kwargs['timeout'] = 90
# Obtain an OpenID Connect (OIDC) token from metadata server or using service
# account.
open_id_connect_token = id_token.fetch_id_token(Request(), client_id)
# Fetch the Identity-Aware Proxy-protected URL, including an
# Authorization header containing "Bearer " followed by a
# Google-issued OpenID Connect token for the service account.
resp = requests.request(
method, url,
headers={'Authorization': 'Bearer {}'.format(
open_id_connect_token)}, **kwargs)
if resp.status_code == 403:
raise Exception('Service account does not have permission to '
'access the IAP-protected application.')
elif resp.status_code != 200:
raise Exception(
'Bad response from application: {!r} / {!r} / {!r}'.format(
resp.status_code, resp.headers, resp.text))
else:
return resp.text
Note: The above method works with implicit credentials that can be set by running command: export GOOGLE_APPLICATION_CREDENTIALS=my-local-service-account-key-file.json to set the path to your service account in the environment and then run the python code from the same terminal.
Take a look at this link for more info.

Is there a way to authenticate OAUTH2.0 of google API through terminal?

I am fetching google photos from my account using Google Photo API. Now there is a need for me to execute that php file via terminal, but the problem is that I can't authenticate with Google API in doing so. Is there a way to do this, and if yes, then how shall it be done?
Yes, it is possible, you need an interactive login for the first authentication but then you can save the token and refresh it automatically as required.
I have implemented this class in Python to do just that.
from requests.adapters import HTTPAdapter
from requests_oauthlib import OAuth2Session
from pathlib import Path
from urllib3.util.retry import Retry
from typing import List, Optional
from json import load, dump, JSONDecodeError
import logging
log = logging.getLogger(__name__)
# OAuth endpoints given in the Google API documentation
authorization_base_url = "https://accounts.google.com/o/oauth2/v2/auth"
token_uri = "https://www.googleapis.com/oauth2/v4/token"
class Authorize:
def __init__(
self, scope: List[str], token_file: Path,
secrets_file: Path, max_retries: int = 5
):
""" A very simple class to handle Google API authorization flow
for the requests library. Includes saving the token and automatic
token refresh.
Args:
scope: list of the scopes for which permission will be granted
token_file: full path of a file in which the user token will be
placed. After first use the previous token will also be read in from
this file
secrets_file: full path of the client secrets file obtained from
Google Api Console
"""
self.max_retries = max_retries
self.scope: List[str] = scope
self.token_file: Path = token_file
self.session = None
self.token = None
try:
with secrets_file.open('r') as stream:
all_json = load(stream)
secrets = all_json['installed']
self.client_id = secrets['client_id']
self.client_secret = secrets['client_secret']
self.redirect_uri = secrets['redirect_uris'][0]
self.token_uri = secrets['token_uri']
self.extra = {
'client_id': self.client_id,
'client_secret': self.client_secret}
except (JSONDecodeError, IOError):
print('missing or bad secrets file: {}'.format(secrets_file))
exit(1)
def load_token(self) -> Optional[str]:
try:
with self.token_file.open('r') as stream:
token = load(stream)
except (JSONDecodeError, IOError):
return None
return token
def save_token(self, token: str):
with self.token_file.open('w') as stream:
dump(token, stream)
self.token_file.chmod(0o600)
def authorize(self):
""" Initiates OAuth2 authentication and authorization flow
"""
token = self.load_token()
if token:
self.session = OAuth2Session(self.client_id, token=token,
auto_refresh_url=self.token_uri,
auto_refresh_kwargs=self.extra,
token_updater=self.save_token)
else:
self.session = OAuth2Session(self.client_id, scope=self.scope,
redirect_uri=self.redirect_uri,
auto_refresh_url=self.token_uri,
auto_refresh_kwargs=self.extra,
token_updater=self.save_token)
# Redirect user to Google for authorization
authorization_url, _ = self.session.authorization_url(
authorization_base_url,
access_type="offline",
prompt="select_account")
print('Please go here and authorize,', authorization_url)
# Get the authorization verifier code from the callback url
response_code = input('Paste the response token here:')
# Fetch the access token
self.token = self.session.fetch_token(
self.token_uri, client_secret=self.client_secret,
code=response_code)
self.save_token(self.token)
# note we want retries on POST as well, need to review this once we
# start to do methods that write to Google Photos
retries = Retry(total=self.max_retries,
backoff_factor=0.1,
status_forcelist=[500, 502, 503, 504],
method_whitelist=frozenset(['GET', 'POST']),
raise_on_status=False)
self.session.mount('https://', HTTPAdapter(max_retries=retries))

Django rest framework working with django oauth toolkit

I'm creating an API using Django rest framework and I'm adding oauth2 authentication.
I was able to set it up correctly and I can get the token to access my API end points. So far everything good.
My question now is how to be a bit more selective in what is protected and what is public. In my API there is a subset of end points that can be accessed by everybody so that they are anonymous users and they can't get the access token in the same way because username and password doesn't exists.
Here is the related content in my settings.py:
OAUTH2_PROVIDER = {
# this is the list of available scopes
'SCOPES': {
'read': 'Read scope',
'write': 'Write scope',
'groups': 'Access to your groups'
}
}
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'oauth2_provider.ext.rest_framework.OAuth2Authentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAdminUser',
),
'PAGE_SIZE': 10
}
INSTALLED_APPS = (
...
'oauth2_provider',
'rest_framework',
...
)
views.py:
# Everything fine for this end point
class PrivateViewSet(viewsets.ModelViewSet):
serializer_class = custom_serializers.MySerializer
http_method_names = ['get', 'post', 'head', 'options']
permission_classes = (permissions.IsAuthenticated, custom_permissions.IsAdminOrOwner, TokenHasReadWriteScope)
# Getting the error
class PublicViewSet(viewsets.ModelViewSet):
serializer_class = custom_serializers.MyPublicSerializer
permission_classes = (permissions.AllowAny,)
So when I try to access to "PublicViewSet" end point I get the following error:
{"detail": "Authentication credentials were not provided."}
Is there a way to decide to which end points to apply the oauth2 authorization and keep others open publicly?
You are not been able to access the PublicViewSet endpoint because
it is looking for the token in the setting you provided the
DEFAULT_AUTHENTICATION_CLASSES. It follows the classes.
To avoid this in the view you need to pass an empty authentication_classes.
class PublicViewSet(viewsets.ModelViewSet):
serializer_class = custom_serializers.MyPublicSerializer
authentication_classes = ()
permission_classes = (permissions.AllowAny,)

Google Api Php Client - Google Auth OAuth 2

My code:
if ($client->getAccessToken()) {
$_SESSION['access_token'] = $client->getAccessToken();
$obj_token = json_decode($client->getAccessToken());
$accessToken = $obj_token->access_token;
$token_data = $client->verifyIdToken($accessToken)->getAttributes();
}
Error:
Uncaught exception 'Google_Auth_Exception' with message 'Wrong number of segments in token: ya29.qQH27NhAXVXPJ64txBjhT_j1FNaVCjosyfwUFKpgsn9LBiyNDS7wgDXVDR31y9hvFbM824mrzOcrWA'
$accessToken should be a string with 2 "." but i don't know why $token_data return a29.qQH27NhAXVXPJ64txBjhT_j1FNaVCjosyfwUFKpgsn9LBiyNDS7wgDXVDR31y9hvFbM824mrzOcrWA', that only have 1 "."
You're trying to verify an access_token as an id_token. They are different tokens, serving different purposes. For an id_token example see the code at https://github.com/google/google-api-php-client/blob/master/examples/idtoken.php, so:
$token_data = $client->verifyIdToken()->getAttributes();
assuming that an id_token was returned by Google as part of the handshake in the first place.