Microservice which abstracts out OAuth2/OpenID exchanges and token management from applications
Create a service account and set up directories owned/used by it
$ sudo groupadd auth_microservice
$ sudo useradd -m -g auth_microservice auth_microservice
$ sudo mkdir /etc/auth_microservice
$ sudo chown -R auth_microservice:auth_microservice /etc/auth_microservice
Switch to this account, all app-specific operations will be executed as the service account, which does not have admin privileges
$ sudo su - auth_microservice
[auth_microservice] $
Install python 2.7 or greater. This example uses 3.6.
[auth_microservice] $ git clone https://github.com/heliumdatacommons/auth_microservice.git
[auth_microservice] $ cd auth_microservice/
[auth_microservice] $ cp example/config/* /etc/auth_microservice
[auth_microservice] $ rename 's/.example$//' /etc/auth_microservice/*.example
Edit the files in /etc/auth_microservice
, filling them in with appropriate values.
These are loaded by the microservice on startup and are used as the initial admin api key,
the database user/pass, and the database encryption key.
The app expects (requires) the keys to be stored as 64 bytes of hexadecimal (representing 32 binary bytes). The admin key, the db key, and the db password should all be completely different. A cross-platform way to generate a 64 byte hex string is:
$ python3 -c "import os, binascii; print(binascii.hexlify(os.urandom(32)).decode('utf-8'))"
or without python
$ dd if=/dev/urandom bs=1 count=32 status=none | xxd -p -c1000
If not created beforehand, the app will also create a file named .django.key
in the django project root directory which is used as Django's secret key.
This is used for sessions and Django hash, signing, and token seeding.
For most cases it is fine to just let the app generate its own django key.
The value of this should not be shared. If it is, just delete it and restart the app,
and let it generate a new one.
Now install the app
TODO: recheck the dependencies; they are modified after cleanup
[auth_microservice] $ python3.6 -m venv venv && source venv/bin/activate
[auth_microservice] $ pip install .
[auth_microservice] $ ./manage.py runserver 0.0.0.0:8080
There is database setup that must be performed as a user with sudo access.
First remember the password you set in db.credentials
earlier, then run the following commands:
[auth_microservice] $ exit
$ sudo bash /home/auth_microservice/auth_microservice/example/setup.sh <your-db-password>
$ sudo su - auth_microservice
[auth_microservice] $
uWSGI
is the recommended way to run the server.
uwsgi.ini
is provided with a basic default configuration,
which can be changed to meet other environment conditions.
$ uwsgi --ini ./uwsgi.ini
To expose this on an external interface, we can use any http server to wrap localhost 8000 (default port).
For nginx
, add the following to /etc/nginx/nginx.conf in the http block.
If a conflicting path is already in use on 443, the django app can be placed on a sub-path in nginx, like /auth
:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name test.commonsshare.org;
ssl_certificate /opt/certs/auth_microservice.crt;
ssl_certificate_key /opt/certs/auth_microservice.key;
root /usr/share/nginx/html;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://localhost:8000;
}
}
-
additional_params
: string (default empty) with (extra) parameters for the authorization url. In case of OpenID Connect, most params (likescope
,response_type
, andaccess_type
) are already generated. For OAuth2, there are no default parameters. -
additional_scopes
: list of additional scopes that will be requested via the authorization URL. -
user_name_from_token
: list of token attributes to use asuser_name
(tried in order, first existing attribute wins) (default topreferred_username
andemail
) -
name_from_token
: list of token attributes to use asname
(tried in order, first existing attribute wins) (default toname
)
- prompt: boolean (default True): adds
prompt
parameter forlogin
andconsent
to the authorization url
Unittests use tox
to run the tests (under token_service/tests/cases
) with different combinations of
python
and Django
.
E.g. to run the tests with python 2.7
and Django 1.8
, you use
python2 -m tox -e py27-django18
The testing itself uses pytest
, and you can easily rerun the tests
(without flake8
or coverage tests) using
.tox/py27-django18/bin/pytest
Some interesting option for pytest
are
-vvv
: very verbose, show eg the migrations--traceconfig
to show the pytest plugins (incl theconftest.py
customisation)
- If tests fail with
... django.db.utils.OperationalError: no such column: ...
, you need to update the migrations (see below)
With tox
, it is very easy to make new migrations.
One can generate the migrations using any version of python
and Django
,
but it looks like py36-django20
produces backwards compatible code
(whereas e.g. py27-django18
does not produce py3
compatible code).
HERE=$PWD
SP=.tox/py36-django20/lib/python3.6/site-packages
MANAGE=django/conf/project_template/
export DJANGO_SETTINGS_MODULE=token_service.tests.settings
cd $SP/$MANAGE
cp manage.py.tpl manage.py
PYTHONPATH=$HERE/$SP python3 $HERE/$SP/$MANAGE/manage.py makemigrations token_service
This will create the new migration file in $HERE/$SP/token_service/migrations
and you can copy it back the repo.
It might prompt for questions etc, see example output below
Did you rename api_key.key to api_key.key_hash (a CharField)? [y/N] y
Did you rename token.user_id to token.user (a ForeignKey)? [y/N] y
You are trying to add a non-nullable field 'owner' to api_key without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows)
2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now()
>>> ''
You are trying to add a non-nullable field 'access_token_hash' to token without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows)
2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now()
>>> ''
You are trying to add a non-nullable field 'id' to token without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows)
2) Quit, and let me add a default in models.py
Select an option: 0
Please select a valid option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now()
>>> 1
You are trying to add a non-nullable field 'name' to user without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows)
2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now()
>>> ''
Migrations for 'token_service':
0002_auto_20180907_0526.py:
- Create model Nonce
- Create model OIDCMetadataCache
- Create model PendingCallback
- Create model User_key
- Rename field key on api_key to key_hash
- Rename field user_id on token to user
- Remove field token_id from token
- Remove field user_id from user
- Add field enabled to api_key
- Add field owner to api_key
- Add field access_token_hash to token
- Add field id to token
- Add field name to user
- Alter field id on user
- Alter field user_name on user
- Add field user to user_key
- Add field nonce to token