From e2ab1004b7ece9dfb8942e959fb8ea0e4192d892 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Thu, 10 Nov 2016 15:11:48 -0800 Subject: [PATCH] Add ID token verification helpers. (#82) --- docs/reference/google.oauth2.id_token.rst | 7 ++ docs/reference/google.oauth2.rst | 1 + google/oauth2/id_token.py | 115 ++++++++++++++++++++++ tests/oauth2/test_id_token.py | 112 +++++++++++++++++++++ 4 files changed, 235 insertions(+) create mode 100644 docs/reference/google.oauth2.id_token.rst create mode 100644 google/oauth2/id_token.py create mode 100644 tests/oauth2/test_id_token.py diff --git a/docs/reference/google.oauth2.id_token.rst b/docs/reference/google.oauth2.id_token.rst new file mode 100644 index 000000000..db38b6085 --- /dev/null +++ b/docs/reference/google.oauth2.id_token.rst @@ -0,0 +1,7 @@ +google.oauth2.id_token module +============================= + +.. automodule:: google.oauth2.id_token + :members: + :inherited-members: + :show-inheritance: diff --git a/docs/reference/google.oauth2.rst b/docs/reference/google.oauth2.rst index 9126549b6..adb9403ef 100644 --- a/docs/reference/google.oauth2.rst +++ b/docs/reference/google.oauth2.rst @@ -12,5 +12,6 @@ Submodules .. toctree:: google.oauth2.credentials + google.oauth2.id_token google.oauth2.service_account diff --git a/google/oauth2/id_token.py b/google/oauth2/id_token.py new file mode 100644 index 000000000..968f27142 --- /dev/null +++ b/google/oauth2/id_token.py @@ -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): + """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')) + + +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 + then the audience is not verified. + certs_url (str): The URL that specifies the certificates to use to + 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 + 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): + """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) diff --git a/tests/oauth2/test_id_token.py b/tests/oauth2/test_id_token.py new file mode 100644 index 000000000..f3da6632d --- /dev/null +++ b/tests/oauth2/test_id_token.py @@ -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: + 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)