diff options
-rw-r--r-- | mediagoblin/db/migrations.py | 59 | ||||
-rw-r--r-- | mediagoblin/oauth/__init__.py | 6 | ||||
-rw-r--r-- | mediagoblin/oauth/oauth.py | 43 | ||||
-rw-r--r-- | mediagoblin/oauth/views.py | 2 |
4 files changed, 102 insertions, 8 deletions
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index 00b0add1..d6806813 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -32,6 +32,8 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.sql import and_ from sqlalchemy.schema import UniqueConstraint +from mediagoblin import oauth +from mediagoblin.tools import crypto from mediagoblin.db.extratypes import JSONEncoded, MutationDict from mediagoblin.db.migration_tools import ( RegisterMigration, inspect_table, replace_table_hack) @@ -1616,3 +1618,60 @@ def federation_media_entry(db): )) db.commit() + +@RegisterMigration(36, MIGRATIONS) +def create_oauth1_dummies(db): + """ + Creates a dummy client, request and access tokens. + + This is used when invalid data is submitted but real clients and + access tokens. The use of dummy objects prevents timing attacks. + """ + metadata = MetaData(bind=db.bind) + client_table = inspect_table(metadata, "core__clients") + request_token_table = inspect_table(metadata, "core__request_tokens") + access_token_table = inspect_table(metadata, "core__access_tokens") + + # Whilst we don't rely on the secret key being unique or unknown to prevent + # unauthorized clients from using it to authenticate, we still as an extra + # layer of protection created a cryptographically secure key individual to + # each instance that should never be able to be known. + client_secret = crypto.random_string(50) + request_token_secret = crypto.random_string(50) + request_token_verifier = crypto.random_string(50) + access_token_secret = crypto.random_string(50) + + # Dummy created/updated datetime object + epoc_datetime = datetime.datetime.fromtimestamp(0) + + # Create the dummy Client + db.execute(client_table.insert().values( + id=oauth.DUMMY_CLIENT_ID, + secret=client_secret, + application_type="dummy", + created=epoc_datetime, + updated=epoc_datetime + )) + + # Create the dummy RequestToken + db.execute(request_token_table.insert().values( + token=oauth.DUMMY_REQUEST_TOKEN, + secret=request_token_secret, + client=oauth.DUMMY_CLIENT_ID, + verifier=request_token_verifier, + created=epoc_datetime, + updated=epoc_datetime, + callback="oob" + )) + + # Create the dummy AccessToken + db.execute(access_token_table.insert().values( + token=oauth.DUMMY_ACCESS_TOKEN, + secret=access_token_secret, + request_token=oauth.DUMMY_REQUEST_TOKEN, + created=epoc_datetime, + updated=epoc_datetime + )) + + # Commit the changes + db.commit() diff --git a/mediagoblin/oauth/__init__.py b/mediagoblin/oauth/__init__.py index 719b56e7..6dfafea2 100644 --- a/mediagoblin/oauth/__init__.py +++ b/mediagoblin/oauth/__init__.py @@ -14,3 +14,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +# This represents a dummy ID for the Client, RequestToken and AccessToken +# WARNING: Do not change these without providing a data migration to migrate +# existing dummy clients already in the database. +DUMMY_CLIENT_ID = "dummy-client" +DUMMY_ACCESS_TOKEN = "dummy-access-token" +DUMMY_REQUEST_TOKEN = "dummy-request-token" diff --git a/mediagoblin/oauth/oauth.py b/mediagoblin/oauth/oauth.py index c7951734..2cc4e790 100644 --- a/mediagoblin/oauth/oauth.py +++ b/mediagoblin/oauth/oauth.py @@ -18,6 +18,7 @@ import datetime from oauthlib.common import Request from oauthlib.oauth1 import RequestValidator +from mediagoblin import oauth from mediagoblin.db.models import NonceTimestamp, Client, RequestToken, AccessToken class GMGRequestValidator(RequestValidator): @@ -94,7 +95,8 @@ class GMGRequestValidator(RequestValidator): def validate_client_key(self, client_key, request): """ Verifies client exists with id of client_key """ - client = Client.query.filter_by(id=client_key).first() + client_query = Client.query.filter(Client.id != oauth.DUMMY_CLIENT_ID) + client = client_query.filter_by(id=client_key).first() if client is None: return False @@ -102,15 +104,30 @@ class GMGRequestValidator(RequestValidator): def validate_access_token(self, client_key, token, request): """ Verifies token exists for client with id of client_key """ - client = Client.query.filter_by(id=client_key).first() - token = AccessToken.query.filter_by(token=token) - token = token.first() + # Get the client for the request + client_query = Client.query.filter(Client.id != oauth.DUMMY_CLIENT_ID) + client = client_query.filter_by(id=client_key).first() - if token is None: + # If the client is invalid then it's invalid + if client is None: return False - request_token = RequestToken.query.filter_by(token=token.request_token) - request_token = request_token.first() + # Look up the AccessToken + access_token_query = AccessToken.query.filter( + AccessToken.token != oauth.DUMMY_ACCESS_TOKEN + ) + access_token = access_token_query.filter_by(token=token).first() + + # If there isn't one - we can't validate. + if access_token is None: + return False + + # Check that the client matches the on + request_token_query = RequestToken.query.filter( + RequestToken.token != oauth.DUMMY_REQUEST_TOKEN, + RequestToken.token == access_token.request_token + ) + request_token = request_token_query.first() if client.id != request_token.client: return False @@ -131,6 +148,18 @@ class GMGRequestValidator(RequestValidator): access_token = AccessToken.query.filter_by(token=token).first() return access_token.secret + @property + def dummy_client(self): + return oauth.DUMMY_CLIENT_ID + + @property + def dummy_request_token(self): + return oauth.DUMMY_REQUEST_TOKEN + + @property + def dummy_access_token(self): + return oauth.DUMMY_ACCESS_TOKEN + class GMGRequest(Request): """ Fills in data to produce a oauth.common.Request object from a diff --git a/mediagoblin/oauth/views.py b/mediagoblin/oauth/views.py index 1b4787d6..2bfaab3e 100644 --- a/mediagoblin/oauth/views.py +++ b/mediagoblin/oauth/views.py @@ -211,7 +211,7 @@ def request_token(request): error = "Invalid client_id" return json_response({"error": error}, status=400) - # make request token and return to client + # make request token and return to client request_validator = GMGRequestValidator(authorization) rv = RequestTokenEndpoint(request_validator) tokens = rv.create_request_token(request, authorization) |