Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ID token verification helpers. #82

Merged
merged 2 commits into from
Nov 10, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/reference/google.oauth2.id_token.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
google.oauth2.id_token module
=============================

.. automodule:: google.oauth2.id_token
:members:
:inherited-members:
:show-inheritance:
1 change: 1 addition & 0 deletions docs/reference/google.oauth2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ Submodules
.. toctree::

google.oauth2.credentials
google.oauth2.id_token
google.oauth2.service_account

115 changes: 115 additions & 0 deletions google/oauth2/id_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Copyright 2016 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Google ID Token helpers."""

import json

from six.moves import http_client

from google.auth import exceptions
from google.auth import jwt

# The URL that provides public certificates for verifying ID tokens issued
# by Google's OAuth 2.0 authorization server.
_GOOGLE_OAUTH2_CERTS_URL = 'https://www.googleapis.com/oauth2/v1/certs'

# The URL that provides public certificates for verifying ID tokens issued
# by Firebase and the Google APIs infrastructure
_GOOGLE_APIS_CERTS_URL = (
'https://www.googleapis.com/robot/v1/metadata/x509'
'/securetoken@system.gserviceaccount.com')


def _fetch_certs(request, certs_url):

This comment was marked as spam.

This comment was marked as spam.

"""Fetches certificates.

Google-style cerificate endpoints return JSON in the format of
``{'key id': 'x509 certificate'}``.

Args:
request (google.auth.transport.Request): The object used to make
HTTP requests.
certs_url (str): The certificate endpoint URL.

Returns:
Mapping[str, str]: A mapping of public key ID to x.509 certificate
data.
"""
response = request('GET', certs_url)

if response.status != http_client.OK:
raise exceptions.TransportError(
'Could not fetch certificates at {}'.format(certs_url))

return json.loads(response.data.decode('utf-8'))

This comment was marked as spam.

This comment was marked as spam.



def verify_token(id_token, request, audience=None,
certs_url=_GOOGLE_OAUTH2_CERTS_URL):
"""Verifies an ID token and returns the decoded token.

Args:
id_token (Union[str, bytes]): The encoded token.
request (google.auth.transport.Request): The object used to make
HTTP requests.
audience (str): The audience that this token is intended for. If None

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

then the audience is not verified.
certs_url (str): The URL that specifies the certificates to use to

This comment was marked as spam.

verify the token. This URL should return JSON in the format of
``{'key id': 'x509 certificate'}``.

Returns:
Mapping[str, Any]: The decoded token.
"""
certs = _fetch_certs(request, certs_url)

return jwt.decode(id_token, certs=certs, audience=audience)


def verify_oauth2_token(id_token, request, audience=None):
"""Verifies an ID Token issued by Google's OAuth 2.0 authorization server.

Args:
id_token (Union[str, bytes]): The encoded token.
request (google.auth.transport.Request): The object used to make
HTTP requests.
audience (str): The audience that this token is intended for. This is

This comment was marked as spam.

typically your application's OAuth 2.0 client ID. If None then the
audience is not verified.

Returns:
Mapping[str, Any]: The decoded token.
"""
return verify_token(
id_token, request, audience=audience,
certs_url=_GOOGLE_OAUTH2_CERTS_URL)


def verify_firebase_token(id_token, request, audience=None):

This comment was marked as spam.

This comment was marked as spam.

"""Verifies an ID Token issued by Firebase Authentication.

Args:
id_token (Union[str, bytes]): The encoded token.
request (google.auth.transport.Request): The object used to make
HTTP requests.
audience (str): The audience that this token is intended for. This is
typically your Firebase application ID. If None then the audience
is not verified.

Returns:
Mapping[str, Any]: The decoded token.
"""
return verify_token(
id_token, request, audience=audience, certs_url=_GOOGLE_APIS_CERTS_URL)
112 changes: 112 additions & 0 deletions tests/oauth2/test_id_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Copyright 2014 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json

import mock
import pytest

from google.auth import exceptions
from google.oauth2 import id_token


def make_request(status, data=None):
response = mock.Mock()
response.status = status

if data is not None:

This comment was marked as spam.

This comment was marked as spam.

response.data = json.dumps(data).encode('utf-8')

return mock.Mock(return_value=response)


def test__fetch_certs_success():
certs = {'1': 'cert'}
request = make_request(200, certs)

returned_certs = id_token._fetch_certs(request, mock.sentinel.cert_url)

request.assert_called_once_with('GET', mock.sentinel.cert_url)
assert returned_certs == certs


def test__fetch_certs_failure():
request = make_request(404)

with pytest.raises(exceptions.TransportError):
id_token._fetch_certs(request, mock.sentinel.cert_url)

request.assert_called_once_with('GET', mock.sentinel.cert_url)


@mock.patch('google.auth.jwt.decode', autospec=True)
@mock.patch('google.oauth2.id_token._fetch_certs', autospec=True)
def test_verify_token(_fetch_certs, decode):
result = id_token.verify_token(mock.sentinel.token, mock.sentinel.request)

assert result == decode.return_value
_fetch_certs.assert_called_once_with(
mock.sentinel.request, id_token._GOOGLE_OAUTH2_CERTS_URL)
decode.assert_called_once_with(
mock.sentinel.token,
certs=_fetch_certs.return_value,
audience=None)


@mock.patch('google.auth.jwt.decode', autospec=True)
@mock.patch('google.oauth2.id_token._fetch_certs', autospec=True)
def test_verify_token_args(_fetch_certs, decode):
result = id_token.verify_token(
mock.sentinel.token,
mock.sentinel.request,
audience=mock.sentinel.audience,
certs_url=mock.sentinel.certs_url)

assert result == decode.return_value
_fetch_certs.assert_called_once_with(
mock.sentinel.request, mock.sentinel.certs_url)
decode.assert_called_once_with(
mock.sentinel.token,
certs=_fetch_certs.return_value,
audience=mock.sentinel.audience)


@mock.patch('google.oauth2.id_token.verify_token', autospec=True)
def test_verify_oauth2_token(verify_token):
result = id_token.verify_oauth2_token(
mock.sentinel.token,
mock.sentinel.request,
audience=mock.sentinel.audience)

assert result == verify_token.return_value
verify_token.assert_called_once_with(
mock.sentinel.token,
mock.sentinel.request,
audience=mock.sentinel.audience,
certs_url=id_token._GOOGLE_OAUTH2_CERTS_URL)


@mock.patch('google.oauth2.id_token.verify_token', autospec=True)
def test_verify_firebase_token(verify_token):
result = id_token.verify_firebase_token(
mock.sentinel.token,
mock.sentinel.request,
audience=mock.sentinel.audience)

assert result == verify_token.return_value
verify_token.assert_called_once_with(
mock.sentinel.token,
mock.sentinel.request,
audience=mock.sentinel.audience,
certs_url=id_token._GOOGLE_APIS_CERTS_URL)