Skip to content

Commit

Permalink
T14 registration url (#20)
Browse files Browse the repository at this point in the history
Various prod fixes.
  • Loading branch information
dolsysmith authored Nov 10, 2022
1 parent 24af7f3 commit cc3729e
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 17 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ORCID middleware to enable our researchers to designate GW as a trusted partner
1. Create your secure key and certificate for SAML encryption/decryption: `openssl req -new -x509 -days 3652 -nodes -out sp.crt -keyout sp.key`
- These files should go into an `orcidflask/saml/certs` directory.
2. In the `orcidflask/saml` directory, create a `settings.json` file to provide the metadata for your app and your identity provider, as well as the certificate from your identify provider. You can follow the example on the [python3-saml repository](https://github.com/onelogin/python3-saml) or in `example-settings.json`.
3. Copy the example Flask configuration file and edit it to provide sensitive keys, including the SERVER_KEY, ORCID client ID and ORCID client secret.
3. Copy the example Flask configuration file and edit it to provide sensitive keys, including the SERVER_KEY, ORCID client ID and ORCID client secret. The `SERVER_KEY` should be the key used to encrypt the Flask session objects, as described [here](https://flask.palletsprojects.com/en/2.2.x/config/).
`cp example.config.py config.py`
4. Copy `example.docker-compose.yml` to `docker-compose.yml` and `example.env` to `.env`.
5. Bring up the Docker container(s): `docker-compose up -d`. This will install all necessary dependencies and launch the Flask app with gunicorn on port `8080`. For development, comment out the first three lines under the `volumes` section of the `flask-app` service and uncomment the line `.:/opt/orcid_integration`. This will use the local copy of the Python code.
Expand Down Expand Up @@ -49,3 +49,7 @@ ORCID middleware to enable our researchers to designate GW as a trusted partner
proxy_redirect off;
}
}```
10. To quickly serialize the database as a JSON file, you can run the following command (if outside the container), providing the path to a file in a mounted volume:
```
docker exec -it orcid-integration_flask-app_1 flask serialize-db ./data/token-dump.json
```
5 changes: 4 additions & 1 deletion example.config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
SECRET_KEY = b''
CLIENT_ID = ''
CLIENT_SECRET = ''
SERVER_NAME = ''
SLO_REDIRECT = ''
ORCID_SUCCESS_URL = ''
ORCID_FAILURE_URL = ''
ORCID_FAILURE_URL = ''
# Set to true to prepopulate ORCID's registration for with values from the SAML IdP
PREFILL_REGISTRATION = False
9 changes: 5 additions & 4 deletions example.docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ services:
- POSTGRES_DB_HOST=${POSTGRES_DB_HOST}
- POSTGRES_PORT=${POSTGRES_PORT}
- DB_ENCRYPTION_FILE=${DB_ENCRYPTION_FILE}
- ORCID_SERVER
volumes:
#- ./orcidflask/saml:/opt/orcid_integration/orcidflask/saml
#- ./config.py:/opt/orcid_integration/config.py
#- ./orcidflask/db:/opt/orcid_integration/orcidflask/db
- .:/opt/orcid_integration
- ./orcidflask/saml:/opt/orcid_integration/orcidflask/saml
- ./config.py:/opt/orcid_integration/config.py
- ./orcidflask/db:/opt/orcid_integration/orcidflask/db
#- .:/opt/orcid_integration
2 changes: 2 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ POSTGRES_DB=orcidig
POSTGRES_DB_HOST=db
POSTGRES_PORT=5432
DB_ENCRYPTION_FILE=/opt/orcid_integration/orcidflask/db/db-encrypt.key
# Values are sandbox or prod
ORCID_SERVER=sandbox
7 changes: 5 additions & 2 deletions orcid_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,24 @@ def prepare_token_payload(code: str):
'code': code,
'redirect_uri': url_for('orcid_redirect', _external=True, _scheme='https')}

def extract_saml_user_data(session):
def extract_saml_user_data(session, populate=True):
'''
Extracts name and email attributes from the samlUserData object.
:param session: a Flask session object
:param populate: set to False to turn off this feature (returns attributes mapped to empty strings)
'''
saml_attrs = ['emailaddress', 'firstname', 'lastname']
saml_data = {}
if 'samlUserdata' in session:
if 'samlUserdata' in session and populate:
user_data = session['samlUserdata']
for saml_attr in saml_attrs:
value = user_data.get(saml_attr)
# Name and email attributes appear to be arrays; not sure if there can be more than one element
if value:
value = value[0]
saml_data[saml_attr] = value
else:
saml_data = {s: '' for s in saml_attrs}
return saml_data

def new_encryption_key(file, replace=False):
Expand Down
27 changes: 24 additions & 3 deletions orcidflask/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
import os
import click
from orcid_utils import load_encryption_key, new_encryption_key
import json

app = Flask(__name__)
# load default configs from default_settings.py
app.config.from_object('orcidflask.default_settings')
# load sensitive config settings
app.config.from_envvar('ORCIDFLASK_SETTINGS')
# Set the ORCID URL based on the setting in default_settings.py
if os.getenv('ORCID_SERVER') == 'sandbox':
base_url = 'https://sandbox.orcid.org'
else:
base_url = 'https://orcid.org'
# Personal attributes from SAML metadata definitions
app.config['orcid_auth_url'] = 'https://sandbox.orcid.org/oauth/authorize?client_id={orcid_client_id}&response_type=code&scope={scopes}&redirect_uri={redirect_uri}&family_names={lastname}&given_names={firstname}&email={emailaddress}'
app.config['orcid_token_url'] = 'https://sandbox.orcid.org/oauth/token'
app.config['orcid_auth_url'] = base_url + '/oauth/authorize?client_id={orcid_client_id}&response_type=code&scope={scopes}&redirect_uri={redirect_uri}&family_names={lastname}&given_names={firstname}&email={emailaddress}'
app.config['orcid_register_url'] = base_url + '/register?client_id={orcid_client_id}&response_type=code&scope={scopes}&redirect_uri={redirect_uri}&family_names={lastname}&given_names={firstname}&email={emailaddress}'
app.config['orcid_token_url'] = base_url + '/oauth/token'
app.config['SAML_PATH'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'saml')
app.secret_key = app.config['SECRET_KEY']

Expand All @@ -28,6 +35,7 @@
db.create_all()

import orcidflask.views
from orcidflask.models import Token

@app.cli.command('create-secret-key')
@click.argument('file')
Expand All @@ -42,4 +50,17 @@ def reset_db():
'''
Resets the associated database by dropping all tables. Warning: for development purposes only. Do not run on a production instance without first backing up the database, as this command will result in the loss of all data.
'''
db.drop_all()
db.drop_all()

@app.cli.command('serialize-db')
@click.argument('file', type=click.File('w'))
def serialize_db(file):
'''
Serializes the database as a JSON dump. Argument should be the path to a file, preferably in a volume mapped to the container, such as /opt/orcid_integration/data
'''
# get all records from the database
records = Token.query.all()
# convert to Python dicts
records = [record.to_dict() for record in records]
json.dump(records, file)

10 changes: 10 additions & 0 deletions orcidflask/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,13 @@ class Token(db.Model):
def __repr__(self):
return '<User %r, access_token=%r, token_scope=%r, orcid=%r' % \
(self.userId, self.access_token, self.token_scope, self.orcid)

def to_dict(self):
'''
Returns the record as a Python dict
'''
record = {column.name: getattr(self, column.name)
for column in self.__table__.columns}
# Convert timestamp to string
record['timestamp'] = record['timestamp'].isoformat()
return record
18 changes: 12 additions & 6 deletions orcidflask/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ def index():
success_slo = False
attributes = False
paint_logout = False

# Flag in URL params for registering new users
register = 'register' in request.args
# Initiating the SSO process
if 'sso' in request.args:
# Redirect to ORCID login upon successful SSO
return redirect(auth.login(return_to=url_for('orcid_login')))
return redirect(auth.login(return_to=url_for('orcid_login', scopes='/read-limited', register=register, _external=True, _scheme='https')))
# Initiating the SLO process
elif 'slo' in request.args:
metadata = get_metadata_from_session(session)
Expand Down Expand Up @@ -71,7 +72,7 @@ def index():
# Redirect for login if no params provided
else:
# Remove the scopes param in order to solicit scopes from users
return redirect(auth.login(return_to=url_for('orcid_login', scopes='/read-limited')))
return redirect(auth.login(return_to=url_for('orcid_login', scopes='/read-limited', register=register, _external=True, _scheme='https')))

# Redirect from logout process
return redirect(app.config['SLO_REDIRECT'])
Expand Down Expand Up @@ -106,17 +107,22 @@ def orcid_login():
See the example here: https://github.com/onelogin/python3-saml/blob/master/demo-flask/index.py
'''
scopes = request.args.get('scopes')
register = request.args.get('register')
# If no SAML attributes, redirect for SSO
if not session.get('samlNameId'):
return redirect(url_for('index'))
return redirect(url_for('index', _external=True, _scheme='https'))
# If the scopes param is part of the request, we're not using the form
elif scopes or request.method == 'POST':
# Get the scopes from the form is not part of the URL
if not scopes:
scopes = ' '.join(request.form.keys())
# Get user data from SAML for registration form
saml_user_data = extract_saml_user_data(session)
return redirect(app.config['orcid_auth_url'].format(orcid_client_id=app.config['CLIENT_ID'],
saml_user_data = extract_saml_user_data(session, populate=app.config['PREFILL_REGISTRATION'])
if register == 'True':
orcid_auth_url = app.config['orcid_register_url']
else:
orcid_auth_url = app.config['orcid_auth_url']
return redirect(orcid_auth_url.format(orcid_client_id=app.config['CLIENT_ID'],
scopes=scopes,
redirect_uri=url_for('orcid_redirect',
_scheme='https',
Expand Down

0 comments on commit cc3729e

Please sign in to comment.