adding generic OAuth2 provider (#685)
* adding support for Okta Oauth2 * renaming to OAuth2 * adding documentation of options * fixing flake8 problems
This commit is contained in:
parent
117009c0a2
commit
0326e1031f
|
@ -244,7 +244,7 @@ For more information about how to use social logins, see: `Satellizer <https://g
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
ACTIVE_PROVIDERS = ["ping", "google"]
|
ACTIVE_PROVIDERS = ["ping", "google", "oauth2"]
|
||||||
|
|
||||||
.. data:: PING_SECRET
|
.. data:: PING_SECRET
|
||||||
:noindex:
|
:noindex:
|
||||||
|
@ -303,6 +303,63 @@ For more information about how to use social logins, see: `Satellizer <https://g
|
||||||
|
|
||||||
PING_AUTH_ENDPOINT = "https://<yourpingserver>/oauth2/authorize"
|
PING_AUTH_ENDPOINT = "https://<yourpingserver>/oauth2/authorize"
|
||||||
|
|
||||||
|
.. data:: OAUTH2_SECRET
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
OAUTH2_SECRET = 'somethingsecret'
|
||||||
|
|
||||||
|
.. data:: OAUTH2_ACCESS_TOKEN_URL
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
OAUTH2_ACCESS_TOKEN_URL = "https://<youroauthserver> /oauth2/v1/authorize"
|
||||||
|
|
||||||
|
|
||||||
|
.. data:: OAUTH2_USER_API_URL
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
OAUTH2_USER_API_URL = "https://<youroauthserver>/oauth2/v1/userinfo"
|
||||||
|
|
||||||
|
.. data:: OAUTH2_JWKS_URL
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
OAUTH2_JWKS_URL = "https://<youroauthserver>/oauth2/v1/keys"
|
||||||
|
|
||||||
|
.. data:: OAUTH2_NAME
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
OAUTH2_NAME = "Example Oauth2 Provider"
|
||||||
|
|
||||||
|
.. data:: OAUTH2_CLIENT_ID
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
OAUTH2_CLIENT_ID = "client-id"
|
||||||
|
|
||||||
|
.. data:: OAUTH2_REDIRECT_URI
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
OAUTH2_REDIRECT_URI = "https://<yourlemurserver>/api/1/auth/oauth2"
|
||||||
|
|
||||||
|
.. data:: OAUTH2_AUTH_ENDPOINT
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
OAUTH2_AUTH_ENDPOINT = "https://<youroauthserver>/oauth2/v1/authorize"
|
||||||
|
|
||||||
.. data:: GOOGLE_CLIENT_ID
|
.. data:: GOOGLE_CLIENT_ID
|
||||||
:noindex:
|
:noindex:
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"""
|
"""
|
||||||
import jwt
|
import jwt
|
||||||
import base64
|
import base64
|
||||||
|
import sys
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from flask import Blueprint, current_app
|
from flask import Blueprint, current_app
|
||||||
|
@ -243,6 +244,128 @@ class Ping(Resource):
|
||||||
return dict(token=create_token(user))
|
return dict(token=create_token(user))
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2(Resource):
|
||||||
|
def __init__(self):
|
||||||
|
self.reqparse = reqparse.RequestParser()
|
||||||
|
super(OAuth2, self).__init__()
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
self.reqparse.add_argument('clientId', type=str, required=True, location='json')
|
||||||
|
self.reqparse.add_argument('redirectUri', type=str, required=True, location='json')
|
||||||
|
self.reqparse.add_argument('code', type=str, required=True, location='json')
|
||||||
|
|
||||||
|
args = self.reqparse.parse_args()
|
||||||
|
|
||||||
|
# take the information we have received from the provider to create a new request
|
||||||
|
params = {
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'scope': 'openid email profile groups',
|
||||||
|
'redirect_uri': args['redirectUri'],
|
||||||
|
'code': args['code'],
|
||||||
|
}
|
||||||
|
|
||||||
|
# you can either discover these dynamically or simply configure them
|
||||||
|
access_token_url = current_app.config.get('OAUTH2_ACCESS_TOKEN_URL')
|
||||||
|
user_api_url = current_app.config.get('OAUTH2_USER_API_URL')
|
||||||
|
|
||||||
|
# the secret and cliendId will be given to you when you signup for the provider
|
||||||
|
token = '{0}:{1}'.format(args['clientId'], current_app.config.get("OAUTH2_SECRET"))
|
||||||
|
|
||||||
|
basic = base64.b64encode(bytes(token, 'utf-8'))
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'authorization': 'basic {0}'.format(basic.decode('utf-8'))
|
||||||
|
}
|
||||||
|
|
||||||
|
# exchange authorization code for access token.
|
||||||
|
r = requests.post(access_token_url, headers=headers, params=params)
|
||||||
|
id_token = r.json()['id_token']
|
||||||
|
access_token = r.json()['access_token']
|
||||||
|
|
||||||
|
# fetch token public key
|
||||||
|
header_data = fetch_token_header(id_token)
|
||||||
|
jwks_url = current_app.config.get('OAUTH2_JWKS_URL')
|
||||||
|
|
||||||
|
# retrieve the key material as specified by the token header
|
||||||
|
r = requests.get(jwks_url)
|
||||||
|
for key in r.json()['keys']:
|
||||||
|
if key['kid'] == header_data['kid']:
|
||||||
|
secret = get_rsa_public_key(key['n'], key['e'])
|
||||||
|
algo = header_data['alg']
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return dict(message='Key not found'), 403
|
||||||
|
|
||||||
|
# validate your token based on the key it was signed with
|
||||||
|
try:
|
||||||
|
if sys.version_info >= (3, 0):
|
||||||
|
jwt.decode(id_token, secret.decode('utf-8'), algorithms=[algo], audience=args['clientId'])
|
||||||
|
else:
|
||||||
|
jwt.decode(id_token, secret, algorithms=[algo], audience=args['clientId'])
|
||||||
|
except jwt.DecodeError:
|
||||||
|
return dict(message='Token is invalid'), 403
|
||||||
|
except jwt.ExpiredSignatureError:
|
||||||
|
return dict(message='Token has expired'), 403
|
||||||
|
except jwt.InvalidTokenError:
|
||||||
|
return dict(message='Token is invalid'), 403
|
||||||
|
|
||||||
|
headers = {'authorization': 'Bearer {0}'.format(access_token)}
|
||||||
|
|
||||||
|
# retrieve information about the current user.
|
||||||
|
r = requests.get(user_api_url, headers=headers)
|
||||||
|
profile = r.json()
|
||||||
|
|
||||||
|
user = user_service.get_by_email(profile['email'])
|
||||||
|
metrics.send('successful_login', 'counter', 1)
|
||||||
|
|
||||||
|
# update their google 'roles'
|
||||||
|
roles = []
|
||||||
|
|
||||||
|
role = role_service.get_by_name(profile['email'])
|
||||||
|
if not role:
|
||||||
|
role = role_service.create(profile['email'], description='This is a user specific role')
|
||||||
|
roles.append(role)
|
||||||
|
|
||||||
|
# if we get an sso user create them an account
|
||||||
|
if not user:
|
||||||
|
# every user is an operator (tied to a default role)
|
||||||
|
if current_app.config.get('LEMUR_DEFAULT_ROLE'):
|
||||||
|
v = role_service.get_by_name(current_app.config.get('LEMUR_DEFAULT_ROLE'))
|
||||||
|
if v:
|
||||||
|
roles.append(v)
|
||||||
|
|
||||||
|
user = user_service.create(
|
||||||
|
profile['name'],
|
||||||
|
get_psuedo_random_string(),
|
||||||
|
profile['email'],
|
||||||
|
True,
|
||||||
|
profile.get('thumbnailPhotoUrl'),
|
||||||
|
roles
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# we add 'lemur' specific roles, so they do not get marked as removed
|
||||||
|
for ur in user.roles:
|
||||||
|
if ur.authority_id:
|
||||||
|
roles.append(ur)
|
||||||
|
|
||||||
|
# update any changes to the user
|
||||||
|
user_service.update(
|
||||||
|
user.id,
|
||||||
|
profile['name'],
|
||||||
|
profile['email'],
|
||||||
|
True,
|
||||||
|
profile.get('thumbnailPhotoUrl'), # incase profile isn't google+ enabled
|
||||||
|
roles
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tell Flask-Principal the identity changed
|
||||||
|
identity_changed.send(current_app._get_current_object(), identity=Identity(user.id))
|
||||||
|
|
||||||
|
return dict(token=create_token(user))
|
||||||
|
|
||||||
|
|
||||||
class Google(Resource):
|
class Google(Resource):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.reqparse = reqparse.RequestParser()
|
self.reqparse = reqparse.RequestParser()
|
||||||
|
@ -317,10 +440,27 @@ class Providers(Resource):
|
||||||
'type': '2.0'
|
'type': '2.0'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
elif provider == "oauth2":
|
||||||
|
active_providers.append({
|
||||||
|
'name': current_app.config.get("OAUTH2_NAME"),
|
||||||
|
'url': current_app.config.get('OAUTH2_REDIRECT_URI'),
|
||||||
|
'redirectUri': current_app.config.get("OAUTH2_REDIRECT_URI"),
|
||||||
|
'clientId': current_app.config.get("OAUTH2_CLIENT_ID"),
|
||||||
|
'responseType': 'code',
|
||||||
|
'scope': ['openid', 'email', 'profile', 'groups'],
|
||||||
|
'scopeDelimiter': ' ',
|
||||||
|
'authorizationEndpoint': current_app.config.get("OAUTH2_AUTH_ENDPOINT"),
|
||||||
|
'requiredUrlParams': ['scope', 'state', 'nonce'],
|
||||||
|
'state': 'STATE',
|
||||||
|
'nonce': get_psuedo_random_string(),
|
||||||
|
'type': '2.0'
|
||||||
|
})
|
||||||
|
|
||||||
return active_providers
|
return active_providers
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(Login, '/auth/login', endpoint='login')
|
api.add_resource(Login, '/auth/login', endpoint='login')
|
||||||
api.add_resource(Ping, '/auth/ping', endpoint='ping')
|
api.add_resource(Ping, '/auth/ping', endpoint='ping')
|
||||||
api.add_resource(Google, '/auth/google', endpoint='google')
|
api.add_resource(Google, '/auth/google', endpoint='google')
|
||||||
|
api.add_resource(OAuth2, '/auth/oauth2', endpoint='oauth2')
|
||||||
api.add_resource(Providers, '/auth/providers', endpoint='providers')
|
api.add_resource(Providers, '/auth/providers', endpoint='providers')
|
||||||
|
|
Loading…
Reference in New Issue