Skip to content

Commit

Permalink
Support all OpenID client_secret_* token endpoint auth methods
Browse files Browse the repository at this point in the history
  • Loading branch information
satterly committed Dec 5, 2021
1 parent 33eade2 commit e397931
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 7 deletions.
44 changes: 41 additions & 3 deletions alerta/auth/oidc.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import json
from datetime import datetime, timedelta
from uuid import uuid4

import jwt
import requests
Expand Down Expand Up @@ -72,10 +74,46 @@ def openid():
'grant_type': 'authorization_code',
'code': request.json['code'],
'redirect_uri': request.json['redirectUri'],
'client_id': request.json['clientId'],
'client_secret': current_app.config['OAUTH2_CLIENT_SECRET'],
}
r = requests.post(token_endpoint, data)

if type(oidc_configuration['token_endpoint_auth_methods_supported']) == list:
token_endpoint_auth_methods = oidc_configuration['token_endpoint_auth_methods_supported']
else:
token_endpoint_auth_methods = [oidc_configuration['token_endpoint_auth_methods_supported']]

preferred_token_auth_method = 'client_secret_post'
for token_auth_method in current_app.config['OIDC_TOKEN_AUTH_METHODS']:
if token_auth_method in token_endpoint_auth_methods:
preferred_token_auth_method = token_auth_method
break

if preferred_token_auth_method == 'client_secret_basic':
auth = (request.json['clientId'], current_app.config['OAUTH2_CLIENT_SECRET'])
r = requests.post(token_endpoint, data, auth=auth)
elif preferred_token_auth_method == 'client_secret_post':
data['client_id'] = request.json['clientId']
data['client_secret'] = current_app.config['OAUTH2_CLIENT_SECRET']
r = requests.post(token_endpoint, data)
elif preferred_token_auth_method == 'client_secret_jwt':
now = datetime.utcnow()
payload = dict(
iss=request.json['clientId'],
sub=request.json['clientId'],
aud=token_endpoint,
jti=str(uuid4()),
exp=(now + timedelta(minutes=5)),
iat=now
)
client_assertion = jwt.encode(
payload=payload,
key=current_app.config['OAUTH2_CLIENT_SECRET'],
algorithm='HS256'
)
data['client_assertion_type'] = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
data['client_assertion'] = client_assertion
r = requests.post(token_endpoint, data)
else:
raise ApiError(f"Token endpoint auth method '{preferred_token_auth_method}' is not supported by Alerta.", 400)
token = r.json()

if 'error' in token:
Expand Down
5 changes: 3 additions & 2 deletions alerta/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@
# }
] # type: List[Dict[str, Any]]

OAUTH2_CLIENT_ID = None # OAuth2 client ID and secret
OAUTH2_CLIENT_SECRET = None
OAUTH2_CLIENT_ID = '' # OAuth2 client ID and secret
OAUTH2_CLIENT_SECRET = ''
ALLOWED_EMAIL_DOMAINS = ['*']

# Amazon Cognito
Expand Down Expand Up @@ -140,6 +140,7 @@
# OpenID Connect
OIDC_ISSUER_URL = None
OIDC_AUTH_URL = None
OIDC_TOKEN_AUTH_METHODS = ['client_secret_basic', 'client_secret_post', 'client_secret_jwt']
OIDC_LOGOUT_URL = None
OIDC_VERIFY_TOKEN = False
OIDC_ROLE_CLAIM = OIDC_CUSTOM_CLAIM = 'roles' # JWT claim name whose value is used in role mapping
Expand Down
77 changes: 75 additions & 2 deletions tests/test_providers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import unittest
from urllib.parse import parse_qs

import requests_mock

Expand All @@ -25,6 +26,8 @@ def test_amazon_cognito(self, m):
'AUTH_REQUIRED': True,
'AUTH_PROVIDER': 'cognito',
'AWS_REGION': 'eu-west-1',
'OAUTH2_CLIENT_ID': '6gms81dft7up216pqnv7pcq194',
'OAUTH2_CLIENT_SECRET': 'oauth2-client-secret',
'COGNITO_USER_POOL_ID': 'eu-west-1_BMkhxuZsn',
'COGNITO_DOMAIN': 'alerta-webui',
'CUSTOMER_VIEWS': True,
Expand Down Expand Up @@ -140,6 +143,12 @@ def test_amazon_cognito(self, m):
self.assertEqual(claims.email_verified, True, claims)
self.assertEqual(claims.customers, ['Alphabet'], claims)

token_endpoint_request = m.request_history[2]
self.assertEqual(
token_endpoint_request.headers.get('Authorization'),
'Basic NmdtczgxZGZ0N3VwMjE2cHFudjdwY3ExOTQ6b2F1dGgyLWNsaWVudC1zZWNyZXQ='
)

@requests_mock.mock()
def test_azure_v1(self, m):

Expand All @@ -148,6 +157,9 @@ def test_azure_v1(self, m):
'AUTH_REQUIRED': True,
'AUTH_PROVIDER': 'openid',
'OIDC_ISSUER_URL': 'https://sts.windows.net/f24341ef-7a6f-4cff-abb7-99a11ab11127/',
'OIDC_TOKEN_AUTH_METHODS': ['client_secret_post', 'client_secret_basic'],
'OAUTH2_CLIENT_ID': '38ba6223-a887-43e2-9f7d-8d539df55f67',
'OAUTH2_CLIENT_SECRET': 'oauth2-client-secret',
'CUSTOMER_VIEWS': True,
}

Expand Down Expand Up @@ -345,6 +357,11 @@ def test_azure_v1(self, m):
self.assertEqual(claims.email_verified, True, claims)
self.assertEqual(claims.customers, ['Alerta IO'], claims)

token_endpoint_request = m.request_history[2]
data = parse_qs(token_endpoint_request.text)
self.assertEqual(data['client_id'], ['38ba6223-a887-43e2-9f7d-8d539df55f67'])
self.assertEqual(data['client_secret'], ['oauth2-client-secret'])

@requests_mock.mock()
def test_azure_v2(self, m):

Expand All @@ -353,6 +370,9 @@ def test_azure_v2(self, m):
'AUTH_REQUIRED': True,
'AUTH_PROVIDER': 'azure',
'AZURE_TENANT': 'common',
'OIDC_TOKEN_AUTH_METHODS': ['client_secret_post', 'client_secret_basic'],
'OAUTH2_CLIENT_ID': '00bb046a-36d9-4411-b93a-2f10fb35f0b5',
'OAUTH2_CLIENT_SECRET': 'oauth2-client-secret',
'CUSTOMER_VIEWS': True,
}

Expand Down Expand Up @@ -576,6 +596,11 @@ def test_azure_v2(self, m):
self.assertEqual(claims.email_verified, True, claims)
self.assertEqual(claims.customers, ['Hotmail'], claims)

token_endpoint_request = m.request_history[2]
data = parse_qs(token_endpoint_request.text)
self.assertEqual(data['client_id'], ['00bb046a-36d9-4411-b93a-2f10fb35f0b5'])
self.assertEqual(data['client_secret'], ['oauth2-client-secret'])

@requests_mock.mock()
def test_github(self, m):

Expand All @@ -584,8 +609,8 @@ def test_github(self, m):
'AUTH_REQUIRED': True,
'AUTH_PROVIDER': 'github',
# 'GITHUB_URL': 'https://github.com',
'OAUTH2_CLIENT_ID': '',
'OAUTH2_CLIENT_SECRET': '',
'OAUTH2_CLIENT_ID': '4f846ebb21eec62ebdf0',
'OAUTH2_CLIENT_SECRET': 'oauth2-client-secret',
# note that for testing ROLE and GROUP are reversed
'GITHUB_ROLE_CLAIM': 'organizations',
'GITHUB_GROUP_CLAIM': 'teams',
Expand Down Expand Up @@ -793,6 +818,8 @@ def test_gitlab(self, m):
'AUTH_REQUIRED': True,
'AUTH_PROVIDER': 'gitlab',
# 'OIDC_ISSUER_URL': 'https://gitlab.com',
'OAUTH2_CLIENT_ID': '765904e9909fc9cc5c448a887849029d999cc2e400e097221bf910be39a16678',
'OAUTH2_CLIENT_SECRET': 'oauth2-client-secret',
# 'OIDC_CUSTOM_CLAIM': 'groups',
'ALLOWED_GITLAB_GROUPS': ['alerta-project'],
'CUSTOMER_VIEWS': True,
Expand Down Expand Up @@ -978,6 +1005,12 @@ def test_gitlab(self, m):
self.assertEqual(claims.email_verified, True, claims)
self.assertEqual(claims.customers, ['Alerta IO'], claims)

token_endpoint_request = m.request_history[2]
self.assertEqual(
token_endpoint_request.headers.get('Authorization'),
'Basic NzY1OTA0ZTk5MDlmYzljYzVjNDQ4YTg4Nzg0OTAyOWQ5OTljYzJlNDAwZTA5NzIyMWJmOTEwYmUzOWExNjY3ODpvYXV0aDItY2xpZW50LXNlY3JldA=='
)

@requests_mock.mock()
def test_google(self, m):

Expand All @@ -986,6 +1019,9 @@ def test_google(self, m):
'AUTH_REQUIRED': True,
'AUTH_PROVIDER': 'google',
# 'OIDC_ISSUER_URL': 'https://accounts.google.com',
'OIDC_TOKEN_AUTH_METHODS': ['client_secret_post', 'client_secret_basic'],
'OAUTH2_CLIENT_ID': '736147134702-glkb1pesv716j1utg4llg7c3rr7nnhli.apps.googleusercontent.com',
'OAUTH2_CLIENT_SECRET': 'oauth2-client-secret',
'CUSTOMER_VIEWS': True,
}

Expand Down Expand Up @@ -1144,6 +1180,11 @@ def test_google(self, m):
self.assertEqual(claims.email_verified, True, claims)
self.assertEqual(claims.customers, ['Google Inc.'], claims)

token_endpoint_request = m.request_history[1]
data = parse_qs(token_endpoint_request.text)
self.assertEqual(data['client_id'], ['736147134702-glkb1pesv716j1utg4llg7c3rr7nnhli.apps.googleusercontent.com'])
self.assertEqual(data['client_secret'], ['oauth2-client-secret'])

@requests_mock.mock()
def test_keycloak(self, m):

Expand All @@ -1153,6 +1194,8 @@ def test_keycloak(self, m):
'AUTH_PROVIDER': 'keycloak',
'KEYCLOAK_URL': 'http://keycloak.local.alerta.io:9090',
'KEYCLOAK_REALM': 'master',
'OAUTH2_CLIENT_ID': 'alerta-ui',
'OAUTH2_CLIENT_SECRET': 'oauth2-client-secret',
'OIDC_CUSTOM_CLAIM': 'roles',
# 'OIDC_ISSUER_URL': 'http://keycloak.local.alerta.io:9090/auth/realms/master',
# 'OIDC_ROLE_CLAIM': 'roles',
Expand Down Expand Up @@ -1376,6 +1419,12 @@ def test_keycloak(self, m):
self.assertEqual(claims.email_verified, True, claims)
self.assertEqual(claims.customers, ['Domain Customer'], claims)

token_endpoint_request = m.request_history[2]
self.assertEqual(
token_endpoint_request.headers.get('Authorization'),
'Basic YWxlcnRhLXVpOm9hdXRoMi1jbGllbnQtc2VjcmV0'
)

@requests_mock.mock()
def test_openid_auth0(self, m):

Expand All @@ -1384,6 +1433,8 @@ def test_openid_auth0(self, m):
'AUTH_REQUIRED': True,
'AUTH_PROVIDER': 'openid',
'OIDC_ISSUER_URL': 'https://dev-66191jdr.eu.auth0.com/',
'OAUTH2_CLIENT_ID': 'LMMEZSlPYKvM14cFxnWNWkC4DgvRk0dZ',
'OAUTH2_CLIENT_SECRET': 'oauth2-client-secret',
'CUSTOMER_VIEWS': True
}

Expand Down Expand Up @@ -1557,6 +1608,12 @@ def test_openid_auth0(self, m):
self.assertEqual(claims.email_verified, False, claims)
self.assertEqual(claims.customers, ['Foo Corp'], claims)

token_endpoint_request = m.request_history[2]
self.assertEqual(
token_endpoint_request.headers.get('Authorization'),
'Basic TE1NRVpTbFBZS3ZNMTRjRnhuV05Xa0M0RGd2UmswZFo6b2F1dGgyLWNsaWVudC1zZWNyZXQ='
)

@requests_mock.mock()
def test_openid_idp(self, m):

Expand All @@ -1565,6 +1622,8 @@ def test_openid_idp(self, m):
'AUTH_REQUIRED': True,
'AUTH_PROVIDER': 'openid',
'OIDC_ISSUER_URL': 'https://dev-66191jdr.eu.auth0.com/',
'OAUTH2_CLIENT_ID': 'LMMEZSlPYKvM14cFxnWNWkC4DgvRk0dZ',
'OAUTH2_CLIENT_SECRET': 'oauth2-client-secret',
}

authorization_grant = """
Expand Down Expand Up @@ -1769,6 +1828,12 @@ def test_openid_idp(self, m):
self.assertEqual(claims.email, 'nfsatterly@gmail.com', claims)
self.assertEqual(claims.email_verified, True, claims)

token_endpoint_request = m.request_history[2]
self.assertEqual(
token_endpoint_request.headers.get('Authorization'),
'Basic TE1NRVpTbFBZS3ZNMTRjRnhuV05Xa0M0RGd2UmswZFo6b2F1dGgyLWNsaWVudC1zZWNyZXQ='
)

@requests_mock.mock()
def test_openid_okta(self, m):

Expand All @@ -1777,6 +1842,8 @@ def test_openid_okta(self, m):
'AUTH_REQUIRED': True,
'AUTH_PROVIDER': 'openid',
'OIDC_ISSUER_URL': 'https://dev-490527.okta.com/oauth2/default',
'OAUTH2_CLIENT_ID': '0oac5s46zwh5crGiH356',
'OAUTH2_CLIENT_SECRET': 'oauth2-client-secret',
'OIDC_ROLE_CLAIM': 'groups',
# 'OIDC_CUSTOM_CLAIM': 'groups',
'CUSTOMER_VIEWS': True,
Expand Down Expand Up @@ -1998,3 +2065,9 @@ def test_openid_okta(self, m):
self.assertEqual(claims.scopes, ['read', 'write'], claims)
self.assertEqual(claims.email_verified, True, claims)
self.assertEqual(claims.customers, ['Alerta Dev'], claims)

token_endpoint_request = m.request_history[2]
self.assertEqual(
token_endpoint_request.headers.get('Authorization'),
'Basic MG9hYzVzNDZ6d2g1Y3JHaUgzNTY6b2F1dGgyLWNsaWVudC1zZWNyZXQ='
)

0 comments on commit e397931

Please sign in to comment.