""" .. module: lemur.auth.views :platform: Unix :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ import jwt import base64 import requests from flask import g, Blueprint, current_app, abort from flask.ext.restful import reqparse, Resource, Api from flask.ext.principal import Identity, identity_changed from lemur.common.crypto import unlock from lemur.auth.permissions import admin_permission from lemur.users import service as user_service from lemur.roles import service as role_service from lemur.certificates import service as cert_service from lemur.auth.service import AuthenticatedResource, create_token, fetch_token_header, get_rsa_public_key mod = Blueprint('auth', __name__) api = Api(mod) class Login(Resource): """ Provides an endpoint for Lemur's basic authentication. It takes a username and password combination and returns a JWT token. This token token is required for each API request and must be provided in the Authorization Header for the request. :: Authorization:Bearer Tokens have a set expiration date. You can inspect the token expiration be base64 decoding the token and inspecting it's contents. .. note:: It is recommended that the token expiration is fairly short lived (hours not days). This will largely depend \ on your uses cases but. It is important to not that there is currently no build in method to revoke a users token \ and force re-authentication. """ def __init__(self): self.reqparse = reqparse.RequestParser() super(Login, self).__init__() def post(self): """ .. http:post:: /auth/login Login with username:password **Example request**: .. sourcecode:: http POST /auth/login HTTP/1.1 Host: example.com Accept: application/json, text/javascript { "username": "test", "password": "test" } **Example response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: text/javascript { "token": "12343243243" } :arg username: username :arg password: password :statuscode 401: invalid credentials :statuscode 200: no error """ self.reqparse.add_argument('username', type=str, required=True, location='json') self.reqparse.add_argument('password', type=str, required=True, location='json') args = self.reqparse.parse_args() if '@' in args['username']: user = user_service.get_by_email(args['username']) else: user = user_service.get_by_username(args['username']) if user and user.check_password(args['password']): # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) return dict(token=create_token(user)) return dict(message='The supplied credentials are invalid'), 401 def get(self): return {'username': g.current_user.username, 'roles': [r.name for r in g.current_user.roles]} class Ping(Resource): """ This class serves as an example of how one might implement an SSO provider for use with Lemur. In this example we use a OpenIDConnect authentication flow, that is essentially OAuth2 underneath. If you have an OAuth2 provider you want to use Lemur there would be two steps: 1. Define your own class that inherits from :class:`flask.ext.restful.Resource` and create the HTTP methods the \ provider uses for it's callbacks. 2. Add or change the Lemur AngularJS Configuration to point to your new provider """ def __init__(self): self.reqparse = reqparse.RequestParser() super(Ping, 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 Meechum to create a new request params = { 'client_id': args['clientId'], 'grant_type': 'authorization_code', 'scope': 'openid email profile address', 'redirect_uri': args['redirectUri'], 'code': args['code'] } # you can either discover these dynamically or simply configure them access_token_url = current_app.config.get('PING_ACCESS_TOKEN_URL') user_api_url = current_app.config.get('PING_USER_API_URL') # the secret and cliendId will be given to you when you signup for meechum basic = base64.b64encode('{0}:{1}'.format(args['clientId'], current_app.config.get("PING_SECRET"))) headers = {'Authorization': 'Basic {0}'.format(basic)} # 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('PING_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: 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 user_params = dict(access_token=access_token, schema='profile') # retrieve information about the current user. r = requests.get(user_api_url, params=user_params) profile = r.json() user = user_service.get_by_email(profile['email']) # update their google 'roles' roles = [] # Legacy edge case - 'admin' has some special privileges associated with it if 'secops@netflix.com' in profile['googleGroups']: roles.append(role_service.get_by_name('admin')) for group in profile['googleGroups']: role = role_service.get_by_name(group) if not role: role = role_service.create(group, description='This is a google group based role created by Lemur') roles.append(role) # if we get an sso user create them an account # we still pick a random password in case sso is down if not user: # every user is an operator (tied to the verisignCA) v = role_service.get_by_name('verisign') if v: roles.append(v) user = user_service.create( profile['email'], cert_service.create_challenge(), 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['email'], 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 Unlock(AuthenticatedResource): def __init__(self): self.reqparse = reqparse.RequestParser() super(Unlock, self).__init__() @admin_permission.require(http_exception=403) def post(self): self.reqparse.add_argument('password', type=str, required=True, location='json') args = self.reqparse.parse_args() unlock(args['password']) return { "message": "You have successfully unlocked this Lemur instance", "type": "success" } api.add_resource(Login, '/auth/login', endpoint='login') api.add_resource(Ping, '/auth/ping', endpoint='ping') api.add_resource(Unlock, '/auth/unlock', endpoint='unlock')