Skip to content

Commit

Permalink
CILogon authentication (#896)
Browse files Browse the repository at this point in the history
  • Loading branch information
skoranda authored Apr 21, 2021
1 parent 1096198 commit 6aed5cb
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ At the moment, supported IDPs include:
- NIH iTrust
- InCommon
- eduGAIN
- CILogon
- Cognito
- Synapse
- Microsoft
Expand Down
9 changes: 9 additions & 0 deletions fence/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from fence.oidc.server import server
from fence.resources.audit_service_client import AuditServiceClient
from fence.resources.aws.boto_manager import BotoManager
from fence.resources.openid.cilogon_oauth2 import CilogonOauth2Client as CilogonClient
from fence.resources.openid.cognito_oauth2 import CognitoOauth2Client as CognitoClient
from fence.resources.openid.google_oauth2 import GoogleOauth2Client as GoogleClient
from fence.resources.openid.microsoft_oauth2 import (
Expand Down Expand Up @@ -373,6 +374,14 @@ def _setup_oidc_clients(app):
oidc["cognito"], HTTP_PROXY=config.get("HTTP_PROXY"), logger=logger
)

# Add OIDC client for CILogon if configured.
if "cilogon" in oidc:
app.cilogon_client = CilogonClient(
config["OPENID_CONNECT"]["cilogon"],
HTTP_PROXY=config.get("HTTP_PROXY"),
logger=logger,
)

# Add OIDC client for multi-tenant fence if configured.
if "fence" in oidc:
app.fence_client = OAuthClient(**config["OPENID_CONNECT"]["fence"])
Expand Down
9 changes: 9 additions & 0 deletions fence/blueprints/login/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from cdislogging import get_logger

from fence.blueprints.login.cilogon import CilogonLogin, CilogonCallback
from fence.blueprints.login.cognito import CognitoLogin, CognitoCallback
from fence.blueprints.login.fence_login import FenceLogin, FenceCallback
from fence.blueprints.login.google import GoogleLogin, GoogleCallback
Expand All @@ -38,6 +39,7 @@
"okta": "okta",
"cognito": "cognito",
"ras": "ras",
"cilogon": "cilogon",
}


Expand Down Expand Up @@ -292,6 +294,13 @@ def provider_info(login_details):
blueprint_api.add_resource(
ShibbolethCallback, "/shib/login", strict_slashes=False
)

if "cilogon" in configured_idps:
blueprint_api.add_resource(CilogonLogin, "/cilogon", strict_slashes=False)
blueprint_api.add_resource(
CilogonCallback, "/cilogon/login", strict_slashes=False
)

return blueprint


Expand Down
21 changes: 21 additions & 0 deletions fence/blueprints/login/cilogon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import flask

from fence.blueprints.login.base import DefaultOAuth2Login, DefaultOAuth2Callback

CILOGON_IDP_NAME = "cilogon"


class CilogonLogin(DefaultOAuth2Login):
def __init__(self):
super(CilogonLogin, self).__init__(
idp_name=CILOGON_IDP_NAME, client=flask.current_app.cilogon_client
)


class CilogonCallback(DefaultOAuth2Callback):
def __init__(self):
super(CilogonCallback, self).__init__(
idp_name=CILOGON_IDP_NAME,
client=flask.current_app.cilogon_client,
username_field="sub",
)
15 changes: 15 additions & 0 deletions fence/config-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,19 @@ OPENID_CONNECT:
# and that IdP is a SAML IdP with no 'email_verified' outgoing claim, but it is safe
# to assume all emails from this SAML IdP are in fact verified, we may set this to True
assume_emails_verified: False
# CILogon subscribers can create and manage OIDC clients using COmanage Registry.
# Free tier users may request OIDC clients at https://cilogon.org/oauth2/register
cilogon:
client_id: ''
client_secret: ''
# When registering the Callback URLs for your CILogon OIDC client be
# sure to include the FULL url for this deployment, including the https:// scheme
# and server FQDN.
redirect_url: '{{BASE_URL}}/login/cilogon/login/'
# if mock is true, will fake a successful login response for login
# WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only)
mock: false
mock_default_user: 'http://cilogon.org/serverT/users/64703'
synapse:
client_id: ''
client_secret: ''
Expand Down Expand Up @@ -279,6 +292,8 @@ LOGIN_OPTIONS: [] # !!! remove the empty list to enable login options!
# - name: 'ORCID Login through other Fence'
# idp: fence
# fence_idp: orcid
# - name: 'CILogon Login'
# idp: cilogon
# - name: 'InCommon Login'
# idp: fence
# fence_idp: shibboleth
Expand Down
51 changes: 51 additions & 0 deletions fence/resources/openid/cilogon_oauth2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from .idp_oauth2 import Oauth2ClientBase


class CilogonOauth2Client(Oauth2ClientBase):
"""
client for interacting with CILogon OIDC
"""

CILOGON_DISCOVERY_URL = "https://cilogon.org/.well-known/openid-configuration"

def __init__(self, settings, logger, HTTP_PROXY=None):
super(CilogonOauth2Client, self).__init__(
settings,
logger,
scope="openid email profile",
discovery_url=self.CILOGON_DISCOVERY_URL,
idp="CILogon",
HTTP_PROXY=HTTP_PROXY,
)

def get_auth_url(self):
"""
Get authorization uri from discovery doc
"""
authorization_endpoint = self.get_value_from_discovery_doc(
"authorization_endpoint", "https://cilogon.org/authorize"
)

uri, _ = self.session.create_authorization_url(
authorization_endpoint, prompt="login"
)

return uri

def get_user_id(self, code):
try:
token_endpoint = self.get_value_from_discovery_doc(
"token_endpoint", "https://cilogon.org/oauth2/token"
)
jwks_endpoint = self.get_value_from_discovery_doc(
"jwks_uri", "https://cilogon.org/oauth2/certs"
)
claims = self.get_jwt_claims_identity(token_endpoint, jwks_endpoint, code)

if claims["sub"]:
return {"sub": claims["sub"]}
else:
return {"error": "Can't get user's CILogon sub"}
except Exception as e:
self.logger.exception("Can't get user info")
return {"error": "Can't get your CILogon sub: {}".format(e)}
12 changes: 9 additions & 3 deletions tests/login/test_login_redirect.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
import pytest


@pytest.mark.parametrize("idp", ["google", "shib", "microsoft", "okta", "orcid", "ras"])
@pytest.mark.parametrize(
"idp", ["google", "shib", "microsoft", "okta", "orcid", "ras", "cilogon"]
)
@mock.patch(
"fence.resources.openid.ras_oauth2.RASOauth2Client.get_value_from_discovery_doc"
)
Expand All @@ -37,7 +39,9 @@ def test_valid_redirect_base(mock_discovery, app, client, idp):
"""


@pytest.mark.parametrize("idp", ["google", "shib", "microsoft", "okta", "orcid", "ras"])
@pytest.mark.parametrize(
"idp", ["google", "shib", "microsoft", "okta", "orcid", "ras", "cilogon"]
)
@mock.patch(
"fence.resources.openid.ras_oauth2.RASOauth2Client.get_value_from_discovery_doc"
)
Expand All @@ -59,7 +63,9 @@ def test_valid_redirect_oauth(mock_discovery, client, oauth_client, idp):
"""


@pytest.mark.parametrize("idp", ["google", "shib", "microsoft", "okta", "orcid", "ras"])
@pytest.mark.parametrize(
"idp", ["google", "shib", "microsoft", "okta", "orcid", "ras", "cilogon"]
)
def test_invalid_redirect_fails(client, idp):
"""
Check that giving a bogus redirect to the login endpoint returns an error.
Expand Down
6 changes: 6 additions & 0 deletions tests/test-fence-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ OPENID_CONNECT:
client_id: ''
client_secret: ''
redirect_url: '{{BASE_URL}}/login/orcid/login'
cilogon:
client_id: ''
client_secret: ''
redirect_url: '{{BASE_URL}}/login/cilogon/login'
ras:
client_id: ''
client_secret: ''
Expand Down Expand Up @@ -193,6 +197,8 @@ LOGIN_OPTIONS:
- name: 'Orcid Login'
idp: fence
fence_idp: orcid
- name: 'CILogon Login'
idp: cilogon
- name: 'Microsoft Login'
idp: microsoft
- name: 'Okta Login'
Expand Down
3 changes: 3 additions & 0 deletions tests/test_audit_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,9 @@ def test_login_log_login_endpoint(
elif idp == "orcid":
mocked_get_user_id = MagicMock()
get_user_id_value = {"orcid": username}
elif idp == "cilogon":
mocked_get_user_id = MagicMock()
get_user_id_value = {"sub": username}
elif idp == "shib":
headers["persistent_id"] = username
idp_name = "itrust"
Expand Down

0 comments on commit 6aed5cb

Please sign in to comment.