def test_validate_redirect(app, sqlalchemy_datastore): """ Test various possible URLs that urlsplit() shows as relative but many browsers will interpret as absolute - and thus have a open-redirect vulnerability. Note this vulnerability only is viable if the application sets autocorrect_location_header = False """ init_app_with_options(app, sqlalchemy_datastore) with app.test_request_context("http://localhost:5001/login"): assert not validate_redirect_url("\\\\\\github.com") assert not validate_redirect_url(" //github.com") assert not validate_redirect_url("\t//github.com") assert not validate_redirect_url("//github.com") # this is normal urlsplit
def post(self): if "aad" not in current_app.config.get("ACTIVE_PROVIDERS"): return "AzureAD is not enabled in the config. See the ACTIVE_PROVIDERS section.", 404 default_state = 'clientId,{client_id},redirectUri,{redirectUri},return_to,{return_to}'.format( client_id=current_app.config.get('AAD_CLIENT_ID'), redirectUri=current_app.config.get('AAD_REDIRECT_URI'), return_to=current_app.config.get('WEB_PATH')) self.reqparse.add_argument('code', type=str, required=True) self.reqparse.add_argument('id_token', type=str, required=True) self.reqparse.add_argument('state', type=str, required=False, default=default_state) args = self.reqparse.parse_args() client_id = args['state'].split(',')[1] redirect_uri = args['state'].split(',')[3] return_to = args['state'].split(',')[5] id_token = args['id_token'] if not validate_redirect_url(return_to): return_to = current_app.config.get('WEB_PATH') # fetch token public key jwks_url = current_app.config.get('AAD_JWKS_URL') # Validate id_token and extract username (email) username = self.validate_id_token(id_token, client_id, jwks_url) user = setup_user(username, '', current_app.config.get('AAD_DEFAULT_ROLE', 'View')) # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) login_user(user) db.session.commit() db.session.refresh(user) return redirect(return_to, code=302)
def post(self): if "aad" not in current_app.config.get("ACTIVE_PROVIDERS"): return "AzureAD is not enabled in the config. See the ACTIVE_PROVIDERS section.", 404 default_state = 'clientId,{client_id},redirectUri,{redirectUri},return_to,{return_to}'.format( client_id=current_app.config.get('AAD_CLIENT_ID'), redirectUri=current_app.config.get('AAD_REDIRECT_URI'), return_to=current_app.config.get('WEB_PATH') ) self.reqparse.add_argument('code', type=str, required=True) self.reqparse.add_argument('id_token', type=str, required=True) self.reqparse.add_argument('state', type=str, required=False, default=default_state) args = self.reqparse.parse_args() client_id = args['state'].split(',')[1] redirect_uri = args['state'].split(',')[3] return_to = args['state'].split(',')[5] id_token = args['id_token'] if not validate_redirect_url(return_to): return_to = current_app.config.get('WEB_PATH') # fetch token public key jwks_url = current_app.config.get('AAD_JWKS_URL') # Validate id_token and extract username (email) username = self.validate_id_token(id_token, client_id, jwks_url) user = setup_user(username, '', current_app.config.get('AAD_DEFAULT_ROLE', 'View')) # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) login_user(user) db.session.commit() db.session.refresh(user) return redirect(return_to, code=302)
def validate_next(self, field): from flask_security.utils import validate_redirect_url if field.data and not validate_redirect_url(field.data): field.data = '' flash('INVALID_REDIRECT') raise ValidationError('INVALID_REDIRECT')
def post(self): if "ping" not in current_app.config.get("ACTIVE_PROVIDERS"): return "Ping is not enabled in the config. See the ACTIVE_PROVIDERS section.", 404 default_state = 'clientId,{client_id},redirectUri,{redirectUri},return_to,{return_to}'.format( client_id=current_app.config.get('PING_CLIENT_ID'), redirectUri=current_app.config.get('PING_REDIRECT_URI'), return_to=current_app.config.get('WEB_PATH')) self.reqparse.add_argument('code', type=str, required=True) self.reqparse.add_argument('state', type=str, required=False, default=default_state) args = self.reqparse.parse_args() client_id = args['state'].split(',')[1] redirect_uri = args['state'].split(',')[3] return_to = args['state'].split(',')[5] if not validate_redirect_url(return_to): return_to = current_app.config.get('WEB_PATH') # take the information we have received from the provider to create a new request params = { 'client_id': client_id, 'grant_type': 'authorization_code', 'scope': 'openid email profile address', 'redirect_uri': redirect_uri, '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 the provider basic = base64.b64encode( bytes('{0}:{1}'.format(client_id, current_app.config.get("PING_SECRET")))) headers = {'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_payload(id_token)[0] 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: current_app.logger.debug(id_token) current_app.logger.debug(secret) current_app.logger.debug(algo) jwt.decode(id_token, secret.decode('utf-8'), algorithms=[algo], audience=client_id) 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.query.filter(User.email == profile['email']).first() # if we get an sso user create them an account if not user: user = User(email=profile['email'], active=True, role='View' # profile_picture=profile.get('thumbnailPhotoUrl') ) db.session.add(user) db.session.commit() db.session.refresh(user) # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) login_user(user) return redirect(return_to, code=302)
def post(self): if "google" not in current_app.config.get("ACTIVE_PROVIDERS"): return "Google is not enabled in the config. See the ACTIVE_PROVIDERS section.", 404 default_state = 'clientId,{client_id},redirectUri,{redirectUri},return_to,{return_to}'.format( client_id=current_app.config.get("GOOGLE_CLIENT_ID"), redirectUri=api.url_for(Google), return_to=current_app.config.get('WEB_PATH')) self.reqparse.add_argument('code', type=str, required=True) self.reqparse.add_argument('state', type=str, required=False, default=default_state) args = self.reqparse.parse_args() client_id = args['state'].split(',')[1] redirect_uri = args['state'].split(',')[3] return_to = args['state'].split(',')[5] if not validate_redirect_url(return_to): return_to = current_app.config.get('WEB_PATH') access_token_url = 'https://accounts.google.com/o/oauth2/token' people_api_url = 'https://www.googleapis.com/plus/v1/people/me/openIdConnect' args = self.reqparse.parse_args() # Step 1. Exchange authorization code for access token payload = { 'client_id': client_id, 'grant_type': 'authorization_code', 'redirect_uri': redirect_uri, 'code': args['code'], 'client_secret': current_app.config.get('GOOGLE_SECRET') } r = requests.post(access_token_url, data=payload) token = r.json() # Step 1bis. Validate (some information of) the id token (if necessary) google_hosted_domain = current_app.config.get("GOOGLE_HOSTED_DOMAIN") if google_hosted_domain is not None: current_app.logger.debug( 'We need to verify that the token was issued for this hosted domain: %s ' % (google_hosted_domain)) # Get the JSON Web Token id_token = r.json()['id_token'] current_app.logger.debug('The id_token is: %s' % (id_token)) # Extract the payload (header_data, payload_data) = fetch_token_header_payload(id_token) current_app.logger.debug('id_token.header_data: %s' % (header_data)) current_app.logger.debug('id_token.payload_data: %s' % (payload_data)) token_hd = payload_data.get('hd') if token_hd != google_hosted_domain: current_app.logger.debug('Verification failed: %s != %s' % (token_hd, google_hosted_domain)) return dict(message='Token is invalid %s' % token), 403 current_app.logger.debug('Verification passed') # Step 2. Retrieve information about the current user headers = {'Authorization': 'Bearer {0}'.format(token['access_token'])} r = requests.get(people_api_url, headers=headers) profile = r.json() user = User.query.filter(User.email == profile['email']).first() # if we get an sso user create them an account if not user: user = User(email=profile['email'], active=True, role='View' # profile_picture=profile.get('thumbnailPhotoUrl') ) db.session.add(user) db.session.commit() db.session.refresh(user) # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) login_user(user) return redirect(return_to, code=302)
def post(self): if "ping" not in current_app.config.get("ACTIVE_PROVIDERS"): return "Ping is not enabled in the config. See the ACTIVE_PROVIDERS section.", 404 default_state = 'clientId,{client_id},redirectUri,{redirectUri},return_to,{return_to}'.format( client_id=current_app.config.get('PING_CLIENT_ID'), redirectUri=current_app.config.get('PING_REDIRECT_URI'), return_to=current_app.config.get('WEB_PATH') ) self.reqparse.add_argument('code', type=str, required=True) self.reqparse.add_argument('state', type=str, required=False, default=default_state) args = self.reqparse.parse_args() client_id = args['state'].split(',')[1] redirect_uri = args['state'].split(',')[3] return_to = args['state'].split(',')[5] if not validate_redirect_url(return_to): return_to = current_app.config.get('WEB_PATH') # take the information we have received from the provider to create a new request params = { 'client_id': client_id, 'grant_type': 'authorization_code', 'scope': 'openid email profile address', 'redirect_uri': redirect_uri, '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 the provider basic = base64.b64encode(bytes('{0}:{1}'.format(client_id, current_app.config.get("PING_SECRET")))) headers = {'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_payload(id_token)[0] 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.decode('utf-8'), algorithms=[algo], audience=client_id) 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 = setup_user( profile.get('email'), profile.get('groups', profile.get('googleGroups', [])), current_app.config.get('PING_DEFAULT_ROLE', 'View')) # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) login_user(user) db.session.commit() db.session.refresh(user) return redirect(return_to, code=302)
def post(self): if "google" not in current_app.config.get("ACTIVE_PROVIDERS"): return "Google is not enabled in the config. See the ACTIVE_PROVIDERS section.", 404 default_state = 'clientId,{client_id},redirectUri,{redirectUri},return_to,{return_to}'.format( client_id=current_app.config.get("GOOGLE_CLIENT_ID"), redirectUri=api.url_for(Google), return_to=current_app.config.get('WEB_PATH') ) self.reqparse.add_argument('code', type=str, required=True) self.reqparse.add_argument('state', type=str, required=False, default=default_state) args = self.reqparse.parse_args() client_id = args['state'].split(',')[1] redirect_uri = args['state'].split(',')[3] return_to = args['state'].split(',')[5] if not validate_redirect_url(return_to): return_to = current_app.config.get('WEB_PATH') access_token_url = 'https://accounts.google.com/o/oauth2/token' people_api_url = 'https://www.googleapis.com/plus/v1/people/me/openIdConnect' args = self.reqparse.parse_args() # Step 1. Exchange authorization code for access token payload = { 'client_id': client_id, 'grant_type': 'authorization_code', 'redirect_uri': redirect_uri, 'code': args['code'], 'client_secret': current_app.config.get('GOOGLE_SECRET') } r = requests.post(access_token_url, data=payload) token = r.json() # Step 1bis. Validate (some information of) the id token (if necessary) google_hosted_domain = current_app.config.get("GOOGLE_HOSTED_DOMAIN") if google_hosted_domain is not None: current_app.logger.debug('We need to verify that the token was issued for this hosted domain: %s ' % (google_hosted_domain)) # Get the JSON Web Token id_token = r.json()['id_token'] current_app.logger.debug('The id_token is: %s' % (id_token)) # Extract the payload (header_data, payload_data) = fetch_token_header_payload(id_token) current_app.logger.debug('id_token.header_data: %s' % (header_data)) current_app.logger.debug('id_token.payload_data: %s' % (payload_data)) token_hd = payload_data.get('hd') if token_hd != google_hosted_domain: current_app.logger.debug('Verification failed: %s != %s' % (token_hd, google_hosted_domain)) return dict(message='Token is invalid %s' % token), 403 current_app.logger.debug('Verification passed') # Step 2. Retrieve information about the current user headers = {'Authorization': 'Bearer {0}'.format(token['access_token'])} r = requests.get(people_api_url, headers=headers) profile = r.json() user = setup_user(profile.get('email'), profile.get('groups', []), current_app.config.get('GOOGLE_DEFAULT_ROLE', 'View')) # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) login_user(user) db.session.commit() db.session.refresh(user) return redirect(return_to, code=302)
def validate_next(self, field): if field.data and not validate_redirect_url(field.data): field.data = '' flash(*get_message('INVALID_REDIRECT')) raise ValidationError(get_message('INVALID_REDIRECT')[0])
def post(self): if "google" not in current_app.config.get("ACTIVE_PROVIDERS"): return "Google is not enabled in the config. See the ACTIVE_PROVIDERS section.", 404 default_state = 'clientId,{client_id},redirectUri,{redirectUri},return_to,{return_to}'.format( client_id=current_app.config.get("GOOGLE_CLIENT_ID"), redirectUri=api.url_for(Google), return_to=current_app.config.get('WEB_PATH') ) self.reqparse.add_argument('code', type=str, required=True) self.reqparse.add_argument('state', type=str, required=False, default=default_state) args = self.reqparse.parse_args() client_id = args['state'].split(',')[1] redirect_uri = args['state'].split(',')[3] return_to = args['state'].split(',')[5] if not validate_redirect_url(return_to): return_to = current_app.config.get('WEB_PATH') access_token_url = 'https://accounts.google.com/o/oauth2/token' people_api_url = 'https://www.googleapis.com/plus/v1/people/me/openIdConnect' args = self.reqparse.parse_args() # Step 1. Exchange authorization code for access token payload = { 'client_id': client_id, 'grant_type': 'authorization_code', 'redirect_uri': redirect_uri, 'code': args['code'], 'client_secret': current_app.config.get('GOOGLE_SECRET') } r = requests.post(access_token_url, data=payload) token = r.json() # Step 2. Retrieve information about the current user headers = {'Authorization': 'Bearer {0}'.format(token['access_token'])} r = requests.get(people_api_url, headers=headers) profile = r.json() user = User.query.filter(User.email == profile['email']).first() # if we get an sso user create them an account if not user: user = User( email=profile['email'], active=True, role='View' # profile_picture=profile.get('thumbnailPhotoUrl') ) db.session.add(user) db.session.commit() db.session.refresh(user) # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) login_user(user) return redirect(return_to, code=302)
def post(self): if "okta" not in current_app.config.get("ACTIVE_PROVIDERS"): return "Okta is not enabled in the config. See the ACTIVE_PROVIDERS section.", 404 default_state = 'clientId,{client_id},redirectUri,{redirectUri},return_to,{return_to}'.format( client_id=current_app.config.get('OKTA_CLIENT_ID'), redirectUri=current_app.config.get('OKTA_REDIRECT_URI'), return_to=current_app.config.get('WEB_PATH')) self.reqparse.add_argument('code', type=str, required=False) self.reqparse.add_argument('state', type=str, required=False, default=default_state) args = self.reqparse.parse_args() client_id = args['state'].split(',')[1] redirect_uri = args['state'].split(',')[3] return_to = args['state'].split(',')[5] code = args['code'] if not validate_redirect_url(return_to): return_to = current_app.config.get('WEB_PATH') basic = base64.b64encode( bytes('{0}:{1}'.format( client_id, current_app.config.get("OKTA_CLIENT_SECRET")))) headers = { 'Authorization': 'Basic {0}'.format(basic.decode('utf-8')), 'Content-Type': 'application/x-www-form-urlencoded', } access_token_url = current_app.config.get('OKTA_TOKEN_ENDPOINT') params = { 'code': code, 'grant_type': 'authorization_code', 'redirect_uri': redirect_uri } r = requests.post(access_token_url, headers=headers, params=params) id_token = r.json()['id_token'] # fetch token public key header_data = fetch_token_header_payload(id_token)[0] jwks_url = current_app.config.get('OKTA_JWKS_URI') # 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: valid_token = jwt.decode(id_token, secret.decode('utf-8'), algorithms=[algo], audience=client_id) 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 = setup_user(valid_token['email'], '', current_app.config.get('OKTA_DEFAULT_ROLE', 'View')) # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) login_user(user) db.session.commit() db.session.refresh(user) return redirect(return_to, code=302)
def post(self): if "google" not in current_app.config.get("ACTIVE_PROVIDERS"): return "Google is not enabled in the config. See the ACTIVE_PROVIDERS section.", 404 default_state = 'clientId,{client_id},redirectUri,{redirectUri},return_to,{return_to}'.format( client_id=current_app.config.get("GOOGLE_CLIENT_ID"), redirectUri=api.url_for(Google), return_to=current_app.config.get('WEB_PATH')) self.reqparse.add_argument('code', type=str, required=True) self.reqparse.add_argument('state', type=str, required=False, default=default_state) args = self.reqparse.parse_args() client_id = args['state'].split(',')[1] redirect_uri = args['state'].split(',')[3] return_to = args['state'].split(',')[5] if not validate_redirect_url(return_to): return_to = current_app.config.get('WEB_PATH') access_token_url = 'https://accounts.google.com/o/oauth2/token' if self._isAuthMethod('directory'): auth_method_api_url = 'https://www.googleapis.com/admin/directory/v1/groups' elif self._isAuthMethod('people'): auth_method_api_url = 'https://www.googleapis.com/userinfo/v2/me' else: return dict(message='Auth method not supported'), 403 args = self.reqparse.parse_args() # Step 1. Exchange authorization code for access token payload = { 'client_id': client_id, 'grant_type': 'authorization_code', 'redirect_uri': redirect_uri, 'code': args['code'], 'client_secret': current_app.config.get('GOOGLE_SECRET') } r = requests.post(access_token_url, data=payload) token = r.json() if 'error' in token: raise UnableToIssueGoogleAuthToken(token['error']) # Step 1bis. Validate (some information of) the id token (if necessary) google_hosted_domain = current_app.config.get("GOOGLE_HOSTED_DOMAIN") userKey = None if google_hosted_domain is not None: current_app.logger.debug( 'We need to verify that the token was issued for this hosted domain: %s ' % (google_hosted_domain)) # Get the JSON Web Token id_token = token['id_token'] current_app.logger.debug('The id_token is: %s' % (id_token)) # Extract the payload (header_data, payload_data) = fetch_token_header_payload(id_token) current_app.logger.debug('id_token.header_data: %s' % (header_data)) current_app.logger.debug('id_token.payload_data: %s' % (payload_data)) token_hd = payload_data.get('hd') if token_hd != google_hosted_domain: current_app.logger.debug('Verification failed: %s != %s' % (token_hd, google_hosted_domain)) return dict(message='Token is invalid %s' % token), 403 current_app.logger.debug('Verification passed') userKey = payload_data.get('email') # Step 2. Retrieve information about the current user if self._isAuthMethod('directory'): if not self.credentials.token: current_app.logger.debug( 'Attempting refresh credentials to obtain initial access token' ) self.credentials.refresh(GoogleAuthTransportRequest()) headers = { 'Authorization': 'Bearer {0}'.format(self.credentials.token) } api_url = "%(url)s?domain=%(domain)s&userKey=%(email)s&fields=groups/email" % { 'url': auth_method_api_url, 'domain': google_hosted_domain, 'email': userKey } r = requests.get(api_url, headers=headers) groups = r.json() current_app.logger.debug('authenticated user with groups: %s' % groups) if len(groups.get('groups', [])) == 0: return dict(message='Groups association is invald for %s' % userKey), 403 else: groupsEmails = [o['email'] for o in groups.get('groups', [])] default_role = current_app.config.get('GOOGLE_DEFAULT_ROLE', 'View') if current_app.config.get('GOOGLE_ADMIN_ROLE_GROUP_NAME') and \ current_app.config.get('GOOGLE_ADMIN_ROLE_GROUP_NAME') in groupsEmails: default_role = "Admin" current_app.logger.debug('Authenticating user %s as role %s' % (userKey, default_role)) user = setup_user(userKey, groupsEmails, default_role) # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) login_user(user) db.session.commit() db.session.refresh(user) return redirect(return_to, code=302) elif self._isAuthMethod('people'): headers = { 'Authorization': 'Bearer {0}'.format(token['access_token']) } r = requests.get(auth_method_api_url, headers=headers) r.raise_for_status() profile = r.json() if 'email' not in profile: raise UnableToAccessGoogleEmail() user = setup_user( profile.get('email'), profile.get('groups', []), current_app.config.get('GOOGLE_DEFAULT_ROLE', 'View')) # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) login_user(user) db.session.commit() db.session.refresh(user) return redirect(return_to, code=302)