Skip to content

Commit

Permalink
First commit of CILogon authentication
Browse files Browse the repository at this point in the history
First commit of CILogon authentication. CILogon provides a
standards-compliant OpenID Connect (OAuth 2.0) interface to federated
authentication including InCommon, the Australian Access Federation
(AAF), and eduGAIN. CILogon OpenID Connect (OIDC) client registration
is available to researchers and scholars at
https://cilogon.org/oauth2/register
  • Loading branch information
skoranda committed Apr 8, 2021
1 parent d6fe2b2 commit 14f391e
Show file tree
Hide file tree
Showing 9 changed files with 125 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 @@ -364,6 +365,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 @@ -36,6 +37,7 @@
"microsoft": "microsoft",
"cognito": "cognito",
"ras": "ras",
"cilogon": "cilogon",
}


Expand Down Expand Up @@ -286,6 +288,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
20 changes: 20 additions & 0 deletions fence/blueprints/login/cilogon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import flask

from fence.resources.openid.cilogon_oauth2 import CILOGON_IDP_NAME
from fence.blueprints.login.base import DefaultOAuth2Login, DefaultOAuth2Callback


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 @@ -184,6 +184,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 @@ -270,6 +283,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
53 changes: 53 additions & 0 deletions fence/resources/openid/cilogon_oauth2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from .idp_oauth2 import Oauth2ClientBase

CILOGON_IDP_NAME = "cilogon"


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, state = 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", "orcid", "ras"])
@pytest.mark.parametrize(
"idp", ["google", "shib", "microsoft", "orcid", "ras", "cilogon"]
)
@mock.patch(
"fence.resources.openid.ras_oauth2.RASOauth2Client.get_value_from_discovery_doc"
)
Expand All @@ -29,7 +31,9 @@ def test_valid_redirect_base(mock_discovery, app, client, idp):
assert response.status_code == 302


@pytest.mark.parametrize("idp", ["google", "shib", "microsoft", "orcid", "ras"])
@pytest.mark.parametrize(
"idp", ["google", "shib", "microsoft", "orcid", "ras", "cilogon"]
)
@mock.patch(
"fence.resources.openid.ras_oauth2.RASOauth2Client.get_value_from_discovery_doc"
)
Expand All @@ -44,7 +48,9 @@ def test_valid_redirect_oauth(mock_discovery, client, oauth_client, idp):
assert response.status_code == 302


@pytest.mark.parametrize("idp", ["google", "shib", "microsoft", "orcid", "ras"])
@pytest.mark.parametrize(
"idp", ["google", "shib", "microsoft", "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 @@ -186,6 +190,8 @@ LOGIN_OPTIONS:
- name: 'Orcid Login'
idp: fence
fence_idp: orcid
- name: 'CILogon Login'
idp: cilogon
- name: 'Microsoft Login'
idp: microsoft
- name: 'NIH 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 14f391e

Please sign in to comment.