Skip to content

Commit

Permalink
Merge pull request #1114 from uc-cdis/chore/PPS-301
Browse files Browse the repository at this point in the history
Chore/pps-301
  • Loading branch information
tianj7 authored Jun 28, 2024
2 parents 7532951 + 10bbac6 commit 234638e
Show file tree
Hide file tree
Showing 41 changed files with 1,134 additions and 669 deletions.
4 changes: 2 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@
"filename": "fence/utils.py",
"hashed_secret": "8318df9ecda039deac9868adf1944a29a95c7114",
"is_verified": false,
"line_number": 128
"line_number": 129
}
],
"migrations/versions/a04a70296688_non_unique_client_name.py": [
Expand Down Expand Up @@ -268,7 +268,7 @@
"filename": "tests/conftest.py",
"hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9",
"is_verified": false,
"line_number": 1559
"line_number": 1561
},
{
"type": "Base64 High Entropy String",
Expand Down
4 changes: 4 additions & 0 deletions bin/fence_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,9 @@ def parse_arguments():
help='scopes to include in the token (e.g. "user" or "data")',
)
token_create.add_argument("--exp", help="time in seconds until token expiration")
token_create.add_argument(
"--client_id", help="Client Id, required to generate refresh token"
)

force_link_google = subparsers.add_parser("force-link-google")
force_link_google.add_argument(
Expand Down Expand Up @@ -581,6 +584,7 @@ def main():
username=args.username,
scopes=args.scopes,
expires_in=args.exp,
client_id=args.client_id,
)
token_type = str(args.type).strip().lower()
if token_type == "access_token" or token_type == "access":
Expand Down
26 changes: 5 additions & 21 deletions fence/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import flask
from flask_cors import CORS
from sqlalchemy.orm import scoped_session
from flask import _app_ctx_stack, current_app
from flask import current_app
from werkzeug.local import LocalProxy

from authutils.oauth2.client import OAuthClient
Expand Down Expand Up @@ -364,7 +364,6 @@ def app_config(
_setup_audit_service_client(app)
_setup_data_endpoint_and_boto(app)
_load_keys(app, root_dir)
_set_authlib_cfgs(app)

app.prometheus_counters = {}
if config["ENABLE_PROMETHEUS_METRICS"]:
Expand Down Expand Up @@ -406,24 +405,6 @@ def _load_keys(app, root_dir):
}


def _set_authlib_cfgs(app):
# authlib OIDC settings
# key will need to be added
settings = {"OAUTH2_JWT_KEY": keys.default_private_key(app)}
app.config.update(settings)
config.update(settings)

# only add the following if not already provided
config.setdefault("OAUTH2_JWT_ENABLED", True)
config.setdefault("OAUTH2_JWT_ALG", "RS256")
config.setdefault("OAUTH2_JWT_ISS", app.config["BASE_URL"])
config.setdefault("OAUTH2_PROVIDER_ERROR_URI", "/api/oauth2/errors")
app.config.setdefault("OAUTH2_JWT_ENABLED", True)
app.config.setdefault("OAUTH2_JWT_ALG", "RS256")
app.config.setdefault("OAUTH2_JWT_ISS", app.config["BASE_URL"])
app.config.setdefault("OAUTH2_PROVIDER_ERROR_URI", "/api/oauth2/errors")


def _setup_oidc_clients(app):
configured_idps = config.get("OPENID_CONNECT", {})

Expand Down Expand Up @@ -481,7 +462,10 @@ def _setup_oidc_clients(app):
logger=logger,
)
elif idp == "fence":
app.fence_client = OAuthClient(**settings)
# https://docs.authlib.org/en/latest/client/flask.html#flask-client
app.fence_client = OAuthClient(app)
# https://docs.authlib.org/en/latest/client/frameworks.html
app.fence_client.register(**settings)
else: # generic OIDC implementation
client = Oauth2ClientBase(
settings=settings,
Expand Down
34 changes: 20 additions & 14 deletions fence/blueprints/login/fence_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,22 @@ def __init__(self):

def get(self):
"""Handle ``GET /login/fence``."""
oauth2_redirect_uri = flask.current_app.fence_client.client_kwargs.get(
"redirect_uri"
)

# OAuth class can have mutliple clients
client = flask.current_app.fence_client._clients[
flask.current_app.config["OPENID_CONNECT"]["fence"]["name"]
]

oauth2_redirect_uri = client.client_kwargs.get("redirect_uri")

redirect_url = flask.request.args.get("redirect")
if redirect_url:
validate_redirect(redirect_url)
flask.session["redirect"] = redirect_url
(
authorization_url,
state,
) = flask.current_app.fence_client.generate_authorize_redirect(
oauth2_redirect_uri, prompt="login"
)

rv = client.create_authorization_url(oauth2_redirect_uri, prompt="login")

authorization_url = rv["url"]

# add idp parameter to the authorization URL
if "idp" in flask.request.args:
Expand All @@ -57,7 +60,7 @@ def get(self):
flask.session["shib_idp"] = shib_idp
authorization_url = add_params_to_uri(authorization_url, params)

flask.session["state"] = state
flask.session["state"] = rv["state"]
return flask.redirect(authorization_url)


Expand Down Expand Up @@ -88,16 +91,19 @@ def get(self):
" login page for the original application to continue."
)
# Get the token response and log in the user.
redirect_uri = flask.current_app.fence_client._get_session().redirect_uri
tokens = flask.current_app.fence_client.fetch_access_token(
redirect_uri, **flask.request.args.to_dict()
client_name = config["OPENID_CONNECT"]["fence"].get("name", "fence")
client = flask.current_app.fence_client._clients[client_name]
oauth2_redirect_uri = client.client_kwargs.get("redirect_uri")

tokens = client.fetch_access_token(
oauth2_redirect_uri, **flask.request.args.to_dict()
)

try:
# For multi-Fence setup with two Fences >=5.0.0
id_token_claims = validate_jwt(
tokens["id_token"],
aud=self.client.client_id,
aud=client.client_id,
scope={"openid"},
purpose="id",
attempt_refresh=True,
Expand Down
5 changes: 4 additions & 1 deletion fence/blueprints/login/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ def allowed_login_redirects():
with flask.current_app.db.session as session:
clients = session.query(Client).all()
for client in clients:
allowed.extend(client.redirect_uris)
if isinstance(client.redirect_uris, list):
allowed.extend(client.redirect_uris)
elif isinstance(client.redirect_uris, str):
allowed.append(client.redirect_uris)
return {domain(url) for url in allowed}


Expand Down
16 changes: 14 additions & 2 deletions fence/blueprints/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,14 @@
from fence.utils import clear_cookies
from fence.user import get_current_user
from fence.config import config

from authlib.oauth2.rfc6749.errors import (
InvalidScopeError,
)
from fence.utils import validate_scopes
from cdislogging import get_logger

blueprint = flask.Blueprint("oauth2", __name__)
logger = get_logger(__name__)


@blueprint.route("/authorize", methods=["GET", "POST"])
Expand Down Expand Up @@ -114,14 +119,21 @@ def authorize(*args, **kwargs):
return flask.redirect(login_url)

try:
grant = server.validate_consent_request(end_user=user)
grant = server.get_consent_grant(end_user=user)
except OAuth2Error as e:
raise Unauthorized("Failed to authorize: {}".format(str(e)))

client_id = grant.client.client_id
with flask.current_app.db.session as session:
client = session.query(Client).filter_by(client_id=client_id).first()

# Need to do scope check here now due to our design of putting allowed_scope on client
# Authlib now put allowed scope on OIDC server side which doesn't work with our design without modification to the lib
# Doing the scope check here because both client and grant is available here
# Either Get or Post request
request_scopes = flask.request.args.get("scope") or flask.request.form.get("scope")
validate_scopes(request_scopes, client)

# TODO: any way to get from grant?
confirm = flask.request.form.get("confirm") or flask.request.args.get("confirm")
if client.auto_approve:
Expand Down
17 changes: 5 additions & 12 deletions fence/config-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ OPENID_CONNECT:
# If this fence instance is a client of another fence, fill this cfg out.
# REMOVE if not needed
fence:
# Custom name to display for consent screens. If not provided, will use `fence`.
# If the other fence is using NIH Login, you should make name: `NIH Login`
name: ''
# this api_base_url should be the root url for the OTHER fence
# something like: https://example.com
api_base_url: ''
Expand All @@ -155,9 +158,6 @@ OPENID_CONNECT:
authorize_url: '{{api_base_url}}/oauth2/authorize'
access_token_url: '{{api_base_url}}/oauth2/token'
refresh_token_url: '{{api_base_url}}/oauth2/token'
# Custom name to display for consent screens. If not provided, will use `fence`.
# If the other fence is using NIH Login, you should make name: `NIH Login`
name: ''
# if mock is true, will fake a successful login response for login
# WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only)
mock: false
Expand Down Expand Up @@ -386,16 +386,9 @@ ENABLED_IDENTITY_PROVIDERS: {}


# //////////////////////////////////////////////////////////////////////////////////////
# LIBRARY CONFIGURATION (authlib & flask)
# LIBRARY CONFIGURATION (flask)
# - Already contains reasonable defaults
# //////////////////////////////////////////////////////////////////////////////////////
# authlib-specific configs for OIDC flow and JWTs
# NOTE: the OAUTH2_JWT_KEY cfg gets set automatically by fence if keys are setup
# correctly
OAUTH2_JWT_ALG: 'RS256'
OAUTH2_JWT_ENABLED: true
OAUTH2_JWT_ISS: '{{BASE_URL}}'
OAUTH2_PROVIDER_ERROR_URI: '/api/oauth2/errors'

# used for flask, "path mounted under by the application / web server"
# since we deploy as microservices, fence is typically under {{base}}/user
Expand Down Expand Up @@ -691,7 +684,7 @@ GS_BUCKETS: {}
# bucket3:
# region: 'us-east-1'

# When using the Cleversafe storageclient, whether or not to send verify=true
# When using the Cleversafe storageclient, whether or not to send verify=true
# for requests
VERIFY_CLEVERSAFE_CERT: true

Expand Down
Loading

0 comments on commit 234638e

Please sign in to comment.