aboutsummaryrefslogtreecommitdiff
path: root/oauth2client
diff options
context:
space:
mode:
authorBrendan McCollam <brendan@mccoll.am>2016-08-11 20:28:19 +0100
committerJon Wayne Parrott <jonwayne@google.com>2016-08-11 12:28:19 -0700
commit3614fd147a51e68548bc48295b63a96239735ac5 (patch)
tree866dfe36a5432c3c08b3c81e762ca46f4a883ed6 /oauth2client
parent619dff806e12a616683842447fbac90a76d663a0 (diff)
downloadoauth2client-3614fd147a51e68548bc48295b63a96239735ac5.tar.gz
Add support for RFC7636 PKCE (#588)
RFC7636 extends OAuth2 to include a challenge-response protocol called "Proof Key for Code Exchange" (PKCE) in order to mitigate attacks in situations where clients that cannot protect a client secret (e.g.installed desktop applications).
Diffstat (limited to 'oauth2client')
-rw-r--r--oauth2client/_pkce.py65
-rw-r--r--oauth2client/client.py66
2 files changed, 122 insertions, 9 deletions
diff --git a/oauth2client/_pkce.py b/oauth2client/_pkce.py
new file mode 100644
index 0000000..8f22f57
--- /dev/null
+++ b/oauth2client/_pkce.py
@@ -0,0 +1,65 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# 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.
+
+"""
+Utility functions for implementing Proof Key for Code Exchange (PKCE) by OAuth
+Public Clients
+
+See RFC7636.
+"""
+
+import base64
+import hashlib
+import os
+
+
+def code_verifier(n_bytes=64):
+ """
+ Generates a 'code_verifier' as described in section 4.1 of RFC 7636.
+
+ This is a 'high-entropy cryptographic random string' that will be
+ impractical for an attacker to guess.
+
+ Args:
+ n_bytes: integer between 31 and 96, inclusive. default: 64
+ number of bytes of entropy to include in verifier.
+
+ Returns:
+ Bytestring, representing urlsafe base64-encoded random data.
+ """
+ verifier = base64.urlsafe_b64encode(os.urandom(n_bytes))
+ # https://tools.ietf.org/html/rfc7636#section-4.1
+ # minimum length of 43 characters and a maximum length of 128 characters.
+ if len(verifier) < 43:
+ raise ValueError("Verifier too short. n_bytes must be > 30.")
+ elif len(verifier) > 128:
+ raise ValueError("Verifier too long. n_bytes must be < 97.")
+ else:
+ return verifier
+
+
+def code_challenge(verifier):
+ """
+ Creates a 'code_challenge' as described in section 4.2 of RFC 7636
+ by taking the sha256 hash of the verifier and then urlsafe
+ base64-encoding it.
+
+ Args:
+ verifier: bytestring, representing a code_verifier as generated by
+ code_verifier().
+
+ Returns:
+ Bytestring, representing a urlsafe base64-encoded sha256 hash digest.
+ """
+ return base64.urlsafe_b64encode(hashlib.sha256(verifier).digest())
diff --git a/oauth2client/client.py b/oauth2client/client.py
index d92ec82..2d1f6e8 100644
--- a/oauth2client/client.py
+++ b/oauth2client/client.py
@@ -34,6 +34,7 @@ from six.moves import urllib
import oauth2client
from oauth2client import _helpers
+from oauth2client import _pkce
from oauth2client import clientsecrets
from oauth2client import transport
@@ -1632,7 +1633,9 @@ def credentials_from_code(client_id, client_secret, scope, code,
auth_uri=oauth2client.GOOGLE_AUTH_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
device_uri=oauth2client.GOOGLE_DEVICE_URI,
- token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI):
+ token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI,
+ pkce=False,
+ code_verifier=None):
"""Exchanges an authorization code for an OAuth2Credentials object.
Args:
@@ -1656,6 +1659,15 @@ def credentials_from_code(client_id, client_secret, scope, code,
device_uri: string, URI for device authorization endpoint. For
convenience defaults to Google's endpoints but any OAuth
2.0 provider can be used.
+ pkce: boolean, default: False, Generate and include a "Proof Key
+ for Code Exchange" (PKCE) with your authorization and token
+ requests. This adds security for installed applications that
+ cannot protect a client_secret. See RFC 7636 for details.
+ code_verifier: bytestring or None, default: None, parameter passed
+ as part of the code exchange when pkce=True. If
+ None, a code_verifier will automatically be
+ generated as part of step1_get_authorize_url(). See
+ RFC 7636 for details.
Returns:
An OAuth2Credentials object.
@@ -1666,10 +1678,14 @@ def credentials_from_code(client_id, client_secret, scope, code,
"""
flow = OAuth2WebServerFlow(client_id, client_secret, scope,
redirect_uri=redirect_uri,
- user_agent=user_agent, auth_uri=auth_uri,
- token_uri=token_uri, revoke_uri=revoke_uri,
+ user_agent=user_agent,
+ auth_uri=auth_uri,
+ token_uri=token_uri,
+ revoke_uri=revoke_uri,
device_uri=device_uri,
- token_info_uri=token_info_uri)
+ token_info_uri=token_info_uri,
+ pkce=pkce,
+ code_verifier=code_verifier)
credentials = flow.step2_exchange(code, http=http)
return credentials
@@ -1704,6 +1720,15 @@ def credentials_from_clientsecrets_and_code(filename, scope, code,
cache: An optional cache service client that implements get() and set()
methods. See clientsecrets.loadfile() for details.
device_uri: string, OAuth 2.0 device authorization endpoint
+ pkce: boolean, default: False, Generate and include a "Proof Key
+ for Code Exchange" (PKCE) with your authorization and token
+ requests. This adds security for installed applications that
+ cannot protect a client_secret. See RFC 7636 for details.
+ code_verifier: bytestring or None, default: None, parameter passed
+ as part of the code exchange when pkce=True. If
+ None, a code_verifier will automatically be
+ generated as part of step1_get_authorize_url(). See
+ RFC 7636 for details.
Returns:
An OAuth2Credentials object.
@@ -1807,6 +1832,8 @@ class OAuth2WebServerFlow(Flow):
device_uri=oauth2client.GOOGLE_DEVICE_URI,
token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI,
authorization_header=None,
+ pkce=False,
+ code_verifier=None,
**kwargs):
"""Constructor for OAuth2WebServerFlow.
@@ -1844,6 +1871,15 @@ class OAuth2WebServerFlow(Flow):
require a client to authenticate using a
header value instead of passing client_secret
in the POST body.
+ pkce: boolean, default: False, Generate and include a "Proof Key
+ for Code Exchange" (PKCE) with your authorization and token
+ requests. This adds security for installed applications that
+ cannot protect a client_secret. See RFC 7636 for details.
+ code_verifier: bytestring or None, default: None, parameter passed
+ as part of the code exchange when pkce=True. If
+ None, a code_verifier will automatically be
+ generated as part of step1_get_authorize_url(). See
+ RFC 7636 for details.
**kwargs: dict, The keyword arguments are all optional and required
parameters for the OAuth calls.
"""
@@ -1863,6 +1899,8 @@ class OAuth2WebServerFlow(Flow):
self.device_uri = device_uri
self.token_info_uri = token_info_uri
self.authorization_header = authorization_header
+ self._pkce = pkce
+ self.code_verifier = code_verifier
self.params = _oauth2_web_server_flow_params(kwargs)
@_helpers.positional(1)
@@ -1903,6 +1941,13 @@ class OAuth2WebServerFlow(Flow):
query_params['state'] = state
if self.login_hint is not None:
query_params['login_hint'] = self.login_hint
+ if self._pkce:
+ if not self.code_verifier:
+ self.code_verifier = _pkce.code_verifier()
+ challenge = _pkce.code_challenge(self.code_verifier)
+ query_params['code_challenge'] = challenge
+ query_params['code_challenge_method'] = 'S256'
+
query_params.update(self.params)
return _update_query_params(self.auth_uri, query_params)
@@ -1997,6 +2042,8 @@ class OAuth2WebServerFlow(Flow):
}
if self.client_secret is not None:
post_data['client_secret'] = self.client_secret
+ if self._pkce:
+ post_data['code_verifier'] = self.code_verifier
if device_flow_info is not None:
post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0'
else:
@@ -2054,7 +2101,7 @@ class OAuth2WebServerFlow(Flow):
@_helpers.positional(2)
def flow_from_clientsecrets(filename, scope, redirect_uri=None,
message=None, cache=None, login_hint=None,
- device_uri=None):
+ device_uri=None, pkce=None, code_verifier=None):
"""Create a Flow from a clientsecrets file.
Will create the right kind of Flow based on the contents of the
@@ -2103,10 +2150,11 @@ def flow_from_clientsecrets(filename, scope, redirect_uri=None,
'login_hint': login_hint,
}
revoke_uri = client_info.get('revoke_uri')
- if revoke_uri is not None:
- constructor_kwargs['revoke_uri'] = revoke_uri
- if device_uri is not None:
- constructor_kwargs['device_uri'] = device_uri
+ optional = ('revoke_uri', 'device_uri', 'pkce', 'code_verifier')
+ for param in optional:
+ if locals()[param] is not None:
+ constructor_kwargs[param] = locals()[param]
+
return OAuth2WebServerFlow(
client_info['client_id'], client_info['client_secret'],
scope, **constructor_kwargs)