Skip to content

Commit

Permalink
Revert "Revert "WAZO-67 refresh token""
Browse files Browse the repository at this point in the history
This reverts commit 711d8f9.
  • Loading branch information
pc-m committed Sep 10, 2019
1 parent d099dfc commit 31f0341
Show file tree
Hide file tree
Showing 26 changed files with 531 additions and 39 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -12,3 +12,4 @@ nosetests.xml
pep8.txt
pylint.txt
pycodestyle.txt
unit-tests.xml
38 changes: 38 additions & 0 deletions alembic/versions/7b15f7bd52ba_add_the_refresh_token_table.py
@@ -0,0 +1,38 @@
"""add the refresh_token table
Revision ID: 7b15f7bd52ba
Revises: 7c7c2fc280ca
"""

from alembic import op
import sqlalchemy as sa
from sqlalchemy.schema import Column

# revision identifiers, used by Alembic.
revision = '7b15f7bd52ba'
down_revision = '7c7c2fc280ca'


def upgrade():
op.create_table(
'auth_refresh_token',
Column(
'uuid', sa.String(36), server_default=sa.text('uuid_generate_v4()'), primary_key=True,
),
Column('client_id', sa.Text),
Column('user_uuid', sa.String(36), sa.ForeignKey('auth_user.uuid', ondelete='CASCADE')),
Column('backend', sa.Text),
Column('login', sa.Text),
Column('user_agent', sa.Text),
Column('remote_addr', sa.Text),
)
op.create_unique_constraint(
'auth_refresh_token_client_id_user_uuid',
'auth_refresh_token',
['client_id', 'user_uuid'],
)


def downgrade():
op.drop_table('auth_refresh_token')
@@ -0,0 +1,23 @@
"""add the user-agent and remote_addr to the token
Revision ID: 7c7c2fc280ca
Revises: 63acf2b9a7b9
"""

# revision identifiers, used by Alembic.
revision = '7c7c2fc280ca'
down_revision = '63acf2b9a7b9'

from alembic import op
import sqlalchemy as sa


def upgrade():
op.add_column('auth_token', sa.Column('user_agent', sa.Text, server_default='', default=''))
op.add_column('auth_token', sa.Column('remote_addr', sa.Text, server_default='', default=''))


def downgrade():
op.drop_column('auth_token', 'remote_addr')
op.drop_column('auth_token', 'user_agent')
2 changes: 2 additions & 0 deletions integration_tests/suite/helpers/fixtures/db.py
Expand Up @@ -66,6 +66,8 @@ def wrapper(self, *args, **kwargs):
'expire_t': now + token_args.get('expiration', 120),
'acls': token_args.get('acls', []),
'metadata': token_args.get('metadata', {}),
'user_agent': token_args.get('user_agent', ''),
'remote_addr': token_args.get('remote_addr', ''),
}
session = token_args.get('session', {})

Expand Down
2 changes: 2 additions & 0 deletions integration_tests/suite/test_db_token.py
Expand Up @@ -45,6 +45,8 @@ def test_create(self):
'id': 42,
'msg': 'a string field',
},
'user_agent': 'my-user-agent',
'remote_addr': '192.168.1.1',
}
session = {}
token_uuid, session_uuid = self._token_dao.create(body, session)
Expand Down
69 changes: 69 additions & 0 deletions integration_tests/suite/test_token.py
@@ -0,0 +1,69 @@
# Copyright 2019 The Wazo Authors (see the AUTHORS file)
# SPDX-License-Identifier: GPL-3.0-or-later

from hamcrest import (
assert_that,
calling,
ends_with,
equal_to,
has_entries,
has_key,
has_properties,
not_,
)
from xivo_test_helpers.hamcrest.raises import raises

from .helpers.base import WazoAuthTestCase


class TestTokens(WazoAuthTestCase):

def test_that_a_token_has_a_remote_address_and_user_agent(self):
ua = 'My Test Runner'

post_result = self.client.token.new(expiration=1, user_agent=ua)
assert_that(post_result, has_entries(user_agent=ua, remote_addr=ends_with('.1')))
# Docker host address are always X.X.X.1

get_result = self.client.token.get(post_result['token'])
assert_that(get_result, has_entries(user_agent=ua, remote_addr=ends_with('.1')))

def test_refresh_token(self):
client_id = 'my-test'

result = self.client.token.new(expiration=1, access_type='offline', client_id=client_id)
assert_that(result, has_entries(refresh_token=not_(None)))

refresh_token = result['refresh_token']

result = self.client.token.new(
expiration=1,
refresh_token=refresh_token,
client_id=client_id,
)
assert_that(result, not_(has_key('refresh_token')))

def test_that_only_one_refresh_token_exist_for_each_user_uuid_client_id(self):
client_id = 'two-refresh-token'

result_1 = self.client.token.new(expiration=1, access_type='offline', client_id=client_id)
result_2 = self.client.token.new(expiration=1, access_type='offline', client_id=client_id)

assert_that(result_1['refresh_token'], equal_to(result_2['refresh_token']))

def test_refresh_token_with_the_wrong_client_id(self):
client_id = 'my-test'

result = self.client.token.new(expiration=1, access_type='offline', client_id=client_id)
assert_that(result, has_entries(refresh_token=not_(None)))

refresh_token = result['refresh_token']

assert_that(
calling(self.client.token.new).with_args(
expiration=1,
refresh_token=refresh_token,
client_id='another-client-id',
),
raises(Exception).matching(has_properties(response=has_properties(status_code=401)))
)
2 changes: 2 additions & 0 deletions wazo_auth/controller.py
Expand Up @@ -53,6 +53,7 @@ def __init__(self, config):
self._tenant_tree = services.helpers.CachedTenantTree(dao.tenant)
self._token_service = services.TokenService(config, dao, self._tenant_tree, self._bus_publisher)
self._backends = BackendsProxy()
authentication_service = services.AuthenticationService(dao, self._backends)
email_service = services.EmailService(dao, self._tenant_tree, config, template_formatter)
external_auth_service = services.ExternalAuthService(
dao, self._tenant_tree, config, self._bus_publisher, config['enabled_external_auth_plugins']
Expand Down Expand Up @@ -96,6 +97,7 @@ def __init__(self, config):
self._config['loaded_plugins'] = self._loaded_plugins_names(self._backends)
dependencies = {
'api': api,
'authentication_service': authentication_service,
'backends': self._backends,
'config': config,
'email_service': email_service,
Expand Down
18 changes: 18 additions & 0 deletions wazo_auth/database/models.py
Expand Up @@ -122,11 +122,29 @@ class Token(Base):
issued_t = Column(Integer)
expire_t = Column(Integer)
metadata_ = Column(Text, name='metadata')
user_agent = Column(Text)
remote_addr = Column(Text)

acls = relationship('ACL')
session = relationship('Session')


class RefreshToken(Base):

__tablename__ = 'auth_refresh_token'
__table_args__ = (
UniqueConstraint('client_id', 'user_uuid'),
)

uuid = Column(String(36), server_default=text('uuid_generate_v4()'), primary_key=True)
client_id = Column(Text)
user_uuid = Column(String(36), ForeignKey('auth_user.uuid', ondelete='CASCADE'))
backend = Column(Text)
login = Column(Text)
user_agent = Column(Text)
remote_addr = Column(Text)


class Session(Base):

__tablename__ = 'auth_session'
Expand Down
2 changes: 2 additions & 0 deletions wazo_auth/database/queries/__init__.py
Expand Up @@ -10,6 +10,7 @@
from .tenant import TenantDAO
from .token import TokenDAO
from .user import UserDAO
from .refresh_token import RefreshTokenDAO

from xivo import sqlalchemy_helper

Expand All @@ -22,6 +23,7 @@ class DAO:
'external_auth': ExternalAuthDAO,
'group': GroupDAO,
'policy': PolicyDAO,
'refresh_token': RefreshTokenDAO,
'session': SessionDAO,
'tenant': TenantDAO,
'token': TokenDAO,
Expand Down
46 changes: 46 additions & 0 deletions wazo_auth/database/queries/refresh_token.py
@@ -0,0 +1,46 @@
# Copyright 2019 The Wazo Authors (see the AUTHORS file)
# SPDX-License-Identifier: GPL-3.0-or-later

from sqlalchemy import and_, exc

from wazo_auth import exceptions

from .base import BaseDAO
from ..models import RefreshToken


class RefreshTokenDAO(BaseDAO):

def create(self, body):
refresh_token = RefreshToken(**body)
with self.new_session() as s:
s.add(refresh_token)
try:
s.flush()
except exc.IntegrityError as e:
if e.orig.pgcode == self._UNIQUE_CONSTRAINT_CODE:
constraint = e.orig.diag.constraint_name
if constraint == 'auth_refresh_token_client_id_user_uuid':
s.rollback()
return self._get_existing_refresh_token(
body['client_id'], body['user_uuid']
)
raise

return refresh_token.uuid

def get(self, refresh_token, client_id):
filter_ = and_(RefreshToken.client_id == client_id, RefreshToken.uuid == refresh_token)
with self.new_session() as s:
query = s.query(RefreshToken).filter(filter_)
for refresh_token in query.all():
return {'backend_name': refresh_token.backend, 'login': refresh_token.login}

raise exceptions.UnknownRefreshToken(refresh_token, client_id)

def _get_existing_refresh_token(self, client_id, user_uuid):
filter_ = and_(RefreshToken.client_id == client_id, RefreshToken.user_uuid == user_uuid)
with self.new_session() as s:
query = s.query(RefreshToken).filter(filter_)
for refresh_token in query.all():
return refresh_token.uuid
4 changes: 4 additions & 0 deletions wazo_auth/database/queries/token.py
Expand Up @@ -24,6 +24,8 @@ def create(self, body, session_body):
xivo_uuid=body['xivo_uuid'],
issued_t=int(body['issued_t']),
expire_t=int(body['expire_t']),
user_agent=body['user_agent'],
remote_addr=body['remote_addr'],
metadata_=serialized_metadata,
)
token.acls = [ACL(token_uuid=token.uuid, value=acl) for acl in body.get('acls') or []]
Expand Down Expand Up @@ -56,6 +58,8 @@ def get(self, token_uuid):
'acls': [acl.value for acl in token.acls],
'metadata': json.loads(token.metadata_) if token.metadata_ else {},
'session_uuid': token.session_uuid,
'remote_addr': token.remote_addr,
'user_agent': token.user_agent,
}

raise exceptions.UnknownTokenException()
Expand Down
18 changes: 18 additions & 0 deletions wazo_auth/exceptions.py
Expand Up @@ -5,6 +5,24 @@
from xivo.rest_api_helpers import APIException


class NoSuchBackendException(Exception):

def __init__(self, backend_name):
super().__init__(f'no such backend {backend_name}')


class InvalidUsernamePassword(Exception):

def __init__(self, login):
super().__init__(f'unknown username or password for login {login}')


class UnknownRefreshToken(Exception):

def __init__(self, refresh_token, client_id):
super().__init__(f'unknown refresh_token "{refresh_token}" with client_id "{client_id}"')


class TokenServiceException(Exception):
pass

Expand Down
7 changes: 6 additions & 1 deletion wazo_auth/helpers.py
Expand Up @@ -68,7 +68,12 @@ def get_token(self):
return

self._renew_time = time.time() + self._delay - self._threshold
self._token = self._new_token({'expiration': 3600, 'backend': 'wazo_user'})
self._token = self._new_token({
'expiration': 3600,
'backend': 'wazo_user',
'user_agent': '',
'remote_addr': '127.0.0.1',
})

return self._token.token

Expand Down
42 changes: 39 additions & 3 deletions wazo_auth/plugins/http/tokens/api.yml
Expand Up @@ -6,9 +6,18 @@ paths:
produces:
- application/json
summary: Creates a token
description: 'Creates a valid token for the supplied username and password combination
using the specified backend. The stock backends are: ``wazo_user``,
``ldap_user``. For more details about the backends, see http://documentation.wazo.community/en/latest/system/wazo-auth/stock_plugins.html#backends-plugins'
description: |
Creates a valid token for the supplied username and password combination or refresh_token
using the specified backend.
The stock backends are: ``wazo_user``, ``ldap_user``.
Creating a token with the `access_type` *offline* will also create a refresh token which can be used
to create a new token without specifying the username and password.
The username/password and refresh_token method of authentication are mutually exclusive
For more details about the backends, see http://documentation.wazo.community/en/latest/system/wazo-auth/stock_plugins.html#backends-plugins
operationId: createToken
tags:
- token
Expand All @@ -28,6 +37,33 @@ paths:
default: wazo_user
expiration:
type: integer
access_type:
type: string
description: |
The `access_type` indicates whether your application can refresh the tokens when the user is not
present at the browser. Valid parameter values are *online*, which is the default value, and *offline*
Only one refresh token will be created for a given user with a given `client_id`. The old refresh
for `client_id` will be revoken when creating a new one.
The *client_id* field is required when using the `access_type` *offline*
default: online
enum:
- online
- offline
client_id:
type: string
description: |
The `client_id` is used in conjunction with the `access_type` *offline* to known for which application
a refresh token has been emitted.
*Required when using `access_type: offline`*
refresh_token:
type: string
description: |
The `refresh_token` can be used to get a new access token without using the username/password.
This is useful for client application that should not store the username and password once the
user has logged in a first time.
responses:
'200':
description: "The created token's data"
Expand Down

0 comments on commit 31f0341

Please sign in to comment.