def ensureTokenScopes(token, scope): """ Call this to validate a token scope for endpoints that require tokens other than a user authentication token. Raises an AccessException if the required scopes are not allowed by the given token. :param token: The token object used in the request. :type token: dict :param scope: The required scope or set of scopes. :type scope: `str or list of str` """ tokenModel = Token() if tokenModel.hasScope(token, TokenScope.USER_AUTH): return if not tokenModel.hasScope(token, scope): setCurrentUser(None) if isinstance(scope, six.string_types): scope = (scope,) raise AccessException( 'Invalid token scope.\n' 'Required: %s.\n' 'Allowed: %s' % ( ' '.join(scope), '' if token is None else ' '.join(tokenModel.getAllowedScopes(token))))
def _validateCsrfToken(self, state): """ Tests the CSRF token value in the cookie to authenticate the user as the originator of the OAuth2 login. Raises a RestException if the token is invalid. """ csrfTokenId, _, redirect = state.partition('.') token = Token().load(csrfTokenId, objectId=False, level=AccessType.READ) if token is None: raise RestException('Invalid CSRF token (state="%s").' % state, code=403) Token().remove(token) if token['expires'] < datetime.datetime.utcnow(): raise RestException('Expired CSRF token (state="%s").' % state, code=403) if not redirect: raise RestException('No redirect location (state="%s").' % state) return redirect
def verifyEmail(self, user, token): token = Token().load(token, user=user, level=AccessType.ADMIN, objectId=False, exc=True) delta = (token['expires'] - datetime.datetime.utcnow()).total_seconds() hasScope = Token().hasScope(token, TokenScope.EMAIL_VERIFICATION) if token.get('userId') != user['_id'] or delta <= 0 or not hasScope: raise AccessException('The token is invalid or expired.') user['emailVerified'] = True Token().remove(token) user = self._model.save(user) if self._model.canLogin(user): setCurrentUser(user) authToken = self.sendAuthTokenCookie(user) return { 'user': self._model.filter(user, user), 'authToken': { 'token': authToken['_id'], 'expires': authToken['expires'], 'scope': authToken['scope'] }, 'message': 'Email verification succeeded.' } else: return { 'user': self._model.filter(user, user), 'message': 'Email verification succeeded.' }
def checkTemporaryPassword(self, user, token): token = Token().load(token, user=user, level=AccessType.ADMIN, objectId=False, exc=True) delta = (token['expires'] - datetime.datetime.utcnow()).total_seconds() hasScope = Token().hasScope(token, TokenScope.TEMPORARY_USER_AUTH) if token.get('userId') != user['_id'] or delta <= 0 or not hasScope: raise AccessException( 'The token does not grant temporary access to this user.') # Temp auth is verified, send an actual auth token now. We keep the # temp token around since it can still be used on a subsequent request # to change the password authToken = self.sendAuthTokenCookie(user) return { 'user': self._model.filter(user, user), 'authToken': { 'token': authToken['_id'], 'expires': authToken['expires'], 'temporary': True }, 'message': 'Temporary access token is valid.' }
class Token(Resource): """API Endpoint for non-user tokens in the system.""" def __init__(self): super(Token, self).__init__() self.resourceName = 'token' self._model = TokenModel() self.route('DELETE', ('session', ), self.deleteSession) self.route('GET', ('session', ), self.getSession) self.route('GET', ('current', ), self.currentSession) self.route('GET', ('scopes', ), self.listScopes) @access.public @autoDescribeRoute( Description('Retrieve the current session information.').responseClass( 'Token')) def currentSession(self): return self.getCurrentToken() @access.public @autoDescribeRoute( Description('Get an anonymous session token for the system.').notes( 'If you are logged in, this will return a token associated with that login.' ).responseClass('Token')) def getSession(self): token = self.getCurrentToken() # Only create and send new cookie if token isn't valid or will expire soon if not token: token = self.sendAuthTokenCookie( None, scope=TokenScope.ANONYMOUS_SESSION) return {'token': token['_id'], 'expires': token['expires']} @access.token @autoDescribeRoute( Description('Remove a session from the system.').responseClass( 'Token').notes('Attempts to delete your authentication cookie.')) def deleteSession(self): token = self.getCurrentToken() if token: self._model.remove(token) self.deleteAuthTokenCookie() return {'message': 'Session deleted.'} @access.public @autoDescribeRoute( Description('List all token scopes available in the system.')) def listScopes(self): return TokenScope.listScopes()
def _authorizeInitUpload(event): """ Called when initializing an upload, prior to the default handler. Checks if the user is passing an authorized upload token, and if so, sets the current request-thread user to be whoever created the token. """ token = getCurrentToken() params = event.info['params'] tokenModel = Token() parentType = params.get('parentType') parentId = params.get('parentId', '') requiredScopes = {TOKEN_SCOPE_AUTHORIZED_UPLOAD, 'authorized_upload_folder_%s' % parentId} if parentType == 'folder' and tokenModel.hasScope(token=token, scope=requiredScopes): user = User().load(token['userId'], force=True) setCurrentUser(user)
def getCurrentToken(allowCookie=None): """ Returns the current valid token object that was passed via the token header or parameter, or None if no valid token was passed. :param allowCookie: Normally, authentication via cookie is disallowed to protect against CSRF attacks. If you want to expose an endpoint that can be authenticated with a token passed in the Cookie, set this to True. This should only be used on read-only operations that will not make any changes to data on the server, and only in cases where the user agent behavior makes passing custom headers infeasible, such as downloading data to disk in the browser. In the event that allowCookie is not explicitly passed, it will default to False unless the access.cookie decorator is used. :type allowCookie: bool """ if allowCookie is None: allowCookie = getattr(cherrypy.request, 'girderAllowCookie', False) tokenStr = None if 'token' in cherrypy.request.params: # Token as a parameter tokenStr = cherrypy.request.params.get('token') elif 'Girder-Token' in cherrypy.request.headers: tokenStr = cherrypy.request.headers['Girder-Token'] elif allowCookie and 'girderToken' in cherrypy.request.cookie: tokenStr = cherrypy.request.cookie['girderToken'].value if not tokenStr: return None return Token().load(tokenStr, force=True, objectId=False)
def wrapped(*args, **kwargs): if not rest.getCurrentToken(): raise AccessException( 'You must be logged in or have a valid auth token.') if required: Token().requireScope(rest.getCurrentToken(), scope) return fun(*args, **kwargs)
def createJobToken(self, job, days=7): """ Create a token that can be used just for the management of an individual job, e.g. updating job info, progress, logs, status. """ return Token().createToken(days=days, scope='jobs.job_' + str(job['_id']))
def validate(self, doc): from girderformindlogger.models.token import Token from girderformindlogger.models.user import User if doc['tokenDuration']: doc['tokenDuration'] = float(doc['tokenDuration']) else: doc['tokenDuration'] = None doc['name'] = doc['name'].strip() doc['active'] = bool(doc.get('active', True)) if doc['scope'] is not None: if not isinstance(doc['scope'], (list, tuple)): raise ValidationException('Scope must be a list, or None.') if not doc['scope']: raise ValidationException('Custom scope list must not be empty.') # Ensure only registered scopes are being set admin = User().load(doc['userId'], force=True)['admin'] scopes = TokenScope.scopeIds(admin) unknownScopes = set(doc['scope']) - scopes if unknownScopes: raise ValidationException('Invalid scopes: %s.' % ','.join(unknownScopes)) # Deactivating an already existing token if '_id' in doc and not doc['active']: Token().clearForApiKey(doc) return doc
def createToken(self, key, days=None): """ Create a token using an API key. :param key: The API key (the key itself, not the full document). :type key: str :param days: You may request a token duration up to the token duration of the API key itself, or pass None to use the API key duration. :type days: float or None """ from girderformindlogger.models.setting import Setting from girderformindlogger.models.token import Token from girderformindlogger.models.user import User apiKey = self.findOne({ 'key': key }) if apiKey is None or not apiKey['active']: raise ValidationException('Invalid API key.') cap = apiKey['tokenDuration'] or Setting().get(SettingKey.COOKIE_LIFETIME) days = min(float(days or cap), cap) user = User().load(apiKey['userId'], force=True) # Mark last used stamp apiKey['lastUse'] = datetime.datetime.utcnow() apiKey = self.save(apiKey) token = Token().createToken(user=user, days=days, scope=apiKey['scope'], apiKey=apiKey) return (user, token)
def generateTemporaryPassword(self, email): user = self._model.findOne({'email': self._model.hash(email.lower()), 'email_encrypted': True}) if not user: user = self._model.findOne({'email': email.lower(), 'email_encrypted': {'$ne': True}}) if not user: raise RestException('That email is not registered.') token = Token().createToken(user, days=(15/1440.0), scope=TokenScope.TEMPORARY_USER_AUTH) url = '%s#useraccount/%s/token/%s' % ( mail_utils.getEmailUrlPrefix(), str(user['_id']), str(token['_id'])) html = mail_utils.renderTemplate('temporaryAccess.mako', { 'url': url, 'token': str(token['_id']) }) mail_utils.sendMail( '%s: Temporary access' % Setting().get(SettingKey.BRAND_NAME), html, [email] ) return {'message': 'Sent temporary access email.'}
def acceptInvitationByToken(self, invitation, lang, email, token): """ Accept an invitation. """ currentUser = Token().load( token, force=True, objectId=False, exc=False ).get('userId', None) if currentUser is not None: currentUser = UserModel().load(currentUser, force=True) if currentUser is None: raise AccessException( "Invalid token." ) if invitation.get('role', 'user') == 'owner': AppletModel().receiveOwnerShip(AppletModel().load(invitation['appletId'], force=True), currentUser, email) else: profile = InvitationModel().acceptInvitation(invitation, currentUser, email) # editors should be able to access duplicated applets if invitation.get('role','user') == 'editor' or invitation.get('role', 'user') == 'manager': InvitationModel().accessToDuplicatedApplets(invitation, currentUser, email) InvitationModel().remove(invitation) return profile
def _uploadComplete(event): """ Called after an upload finishes. We check if our current token is a special authorized upload token, and if so, delete it. TODO we could alternatively keep a reference count inside each token that authorized more than a single upload at a time, and just decrement it here. """ token = getCurrentToken() if token and 'authorizedUploadId' in token: user = User().load(token['userId'], force=True) item = Item().load(event.info['file']['itemId'], force=True) # Save the metadata on the item item['description'] = token['authorizedUploadDescription'] item['authorizedUploadEmail'] = token['authorizedUploadEmail'] Item().save(item) text = mail_utils.renderTemplate('authorized_upload.uploadFinished.mako', { 'itemId': item['_id'], 'itemName': item['name'], 'itemDescription': item.get('description', '') }) mail_utils.sendMail('Authorized upload complete', text, [user['email']]) Token().remove(token)
def _storeUploadId(event): """ Called after an upload is first initialized successfully. Sets the authorized upload ID in the token, ensuring it can be used for only this upload. """ returnVal = event.info['returnVal'] token = getCurrentToken() tokenModel = Token() isAuthorizedUpload = tokenModel.hasScope(token, TOKEN_SCOPE_AUTHORIZED_UPLOAD) if isAuthorizedUpload and returnVal.get('_modelType', 'upload') == 'upload': params = event.info['params'] token['scope'].remove(TOKEN_SCOPE_AUTHORIZED_UPLOAD) token['authorizedUploadId'] = returnVal['_id'] token['authorizedUploadDescription'] = params.get('authorizedUploadDescription', '') token['authorizedUploadEmail'] = params.get('authorizedUploadEmail') tokenModel.save(token)
def __init__(self): super(Token, self).__init__() self.resourceName = 'token' self._model = TokenModel() self.route('DELETE', ('session', ), self.deleteSession) self.route('GET', ('session', ), self.getSession) self.route('GET', ('current', ), self.currentSession) self.route('GET', ('scopes', ), self.listScopes)
def _sendVerificationEmail(self, user, email): from girderformindlogger.models.token import Token token = Token().createToken(user, days=1, scope=TokenScope.EMAIL_VERIFICATION) url = '%s#useraccount/%s/verification/%s' % ( mail_utils.getEmailUrlPrefix(), str(user['_id']), str( token['_id'])) text = mail_utils.renderTemplate('emailVerification.mako', {'url': url}) mail_utils.sendMail('Girder: Email verification', text, [email])
def acceptInvitationByToken(self, invitation, token): """ Accept an invitation. """ currentUser = Token().load(token, force=True, objectId=False, exc=False).get('userId') if currentUser is not None: currentUser = UserModel().load(currentUser, force=True) if currentUser is None: raise AccessException( "You must be logged in to accept an invitation.") return (InvitationModel().acceptInvitation(invitation, currentUser))
def changePassword(self, old, new): user = self.getCurrentUser() token = None if not old: raise RestException('Old password must not be empty.') if (not self._model.hasPassword(user) or not self._model._cryptContext.verify(old, user['salt'])): # If not the user's actual password, check for temp access token token = Token().load(old, force=True, objectId=False, exc=False) if (not token or not token.get('userId') or token['userId'] != user['_id'] or not Token().hasScope( token, TokenScope.TEMPORARY_USER_AUTH)): raise AccessException('Old password is incorrect.') self._model.setPassword(user, new) if token: # Remove the temporary access token if one was used Token().remove(token) return {'message': 'Password changed.'}
def acceptInvitationByToken(self, invitation, email, token): """ Accept an invitation. """ currentUser = Token().load(token, force=True, objectId=False, exc=False).get('userId', None) if currentUser is not None: currentUser = UserModel().load(currentUser, force=True) if currentUser is None: raise AccessException("Invalid token.") return (InvitationModel().acceptInvitation(invitation, currentUser, email))
def createAuthorizedUpload(self, folder, params): try: if params.get('duration'): days = int(params.get('duration')) else: days = Setting().get(SettingKey.COOKIE_LIFETIME) except ValueError: raise ValidationException( 'Token duration must be an integer, or leave it empty.') token = Token().createToken( days=days, user=self.getCurrentUser(), scope=(TOKEN_SCOPE_AUTHORIZED_UPLOAD, 'authorized_upload_folder_%s' % folder['_id'])) url = '%s#authorized_upload/%s/%s' % (mail_utils.getEmailUrlPrefix(), folder['_id'], token['_id']) return {'url': url}
def buildHeaders(headers, cookie, user, token, basicAuth, authHeader): from girderformindlogger.models.token import Token headers = headers[:] if cookie is not None: headers.append(('Cookie', cookie)) if user is not None: token = Token().createToken(user) headers.append(('Girder-Token', str(token['_id']))) elif token is not None: if isinstance(token, dict): headers.append(('Girder-Token', token['_id'])) else: headers.append(('Girder-Token', token)) if basicAuth is not None: auth = base64.b64encode(basicAuth.encode('utf8')) headers.append((authHeader, 'Basic %s' % auth.decode())) return headers
def sendAuthTokenCookie(self, user=None, scope=None, token=None, days=None): """ Helper method to send the authentication cookie """ if days is None: days = float(Setting().get(SettingKey.COOKIE_LIFETIME)) if token is None: token = Token().createToken(user, days=days, scope=scope) cookie = cherrypy.response.cookie cookie['girderToken'] = str(token['_id']) cookie['girderToken']['path'] = '/' cookie['girderToken']['expires'] = int(days * 3600 * 24) # CherryPy proxy tools modify the request.base, but not request.scheme, when receiving # X-Forwarded-Proto headers from a reverse proxy if cherrypy.request.scheme == 'https' or cherrypy.request.base.startswith('https'): cookie['girderToken']['secure'] = True return token
def remove(self, user, progress=None, **kwargs): """ Delete a user, and all references to it in the database. :param user: The user document to delete. :type user: dict :param progress: A progress context to record progress on. :type progress: girderformindlogger.utility.progress.ProgressContext or None. """ from girderformindlogger.models.folder import Folder from girderformindlogger.models.group import Group from girderformindlogger.models.token import Token # Delete all authentication tokens owned by this user Token().removeWithQuery({'userId': user['_id']}) # Delete all pending group invites for this user Group().update({'requests': user['_id']}, {'$pull': { 'requests': user['_id'] }}) # Delete all of the folders under this user folderModel = Folder() folders = folderModel.find({ 'parentId': user['_id'], 'parentCollection': 'user' }) for folder in folders: folderModel.remove(folder, progress=progress, **kwargs) # Finally, delete the user document itself AccessControlledModel.remove(self, user) if progress: progress.update(increment=1, message='Deleted user ' + user['login'])
def remove(self, doc): # Clear tokens corresponding to this API key. from girderformindlogger.models.token import Token Token().clearForApiKey(doc) super(ApiKey, self).remove(doc)
def logout(self): token = self.getCurrentToken() if token: Token().remove(token) self.deleteAuthTokenCookie() return {'message': 'Logged out.'}
def testAuthorizedUpload(self): Setting().set(SettingKey.UPLOAD_MINIMUM_CHUNK_SIZE, 1) # Anon access should not work resp = self.request('/authorized_upload', method='POST', params={'folderId': self.privateFolder['_id']}) self.assertStatus(resp, 401) # Create our secure URL resp = self.request('/authorized_upload', method='POST', user=self.admin, params={'folderId': self.privateFolder['_id']}) self.assertStatusOk(resp) parts = resp.json['url'].rsplit('/', 3) tokenId, folderId = parts[-1], parts[-2] token = Token().load(tokenId, force=True, objectId=False) self.assertIsNotNone(token) self.assertEqual(folderId, str(self.privateFolder['_id'])) self.assertEqual( set(token['scope']), { TOKEN_SCOPE_AUTHORIZED_UPLOAD, 'authorized_upload_folder_%s' % self.privateFolder['_id'] }) # Make sure this token doesn't let us upload into a different folder params = { 'parentType': 'folder', 'parentId': self.publicFolder['_id'], 'name': 'hello.txt', 'size': 11, 'mimeType': 'text/plain' } resp = self.request(path='/file', method='POST', params=params, token=tokenId) self.assertStatus(resp, 401) # Initialize upload into correct folder params['parentId'] = self.privateFolder['_id'] resp = self.request(path='/file', method='POST', params=params, token=tokenId) self.assertStatusOk(resp) # We should remove the scope that allows further uploads upload = Upload().load(resp.json['_id']) token = Token().load(tokenId, force=True, objectId=False) self.assertEqual( token['scope'], ['authorized_upload_folder_%s' % self.privateFolder['_id']]) # Authorized upload ID should be present in the token self.assertEqual(token['authorizedUploadId'], upload['_id']) # Attempting to initialize new uploads using the token should fail resp = self.request(path='/file', method='POST', params=params, token=tokenId) self.assertStatus(resp, 401) # Uploading a chunk should work with the token resp = self.request(path='/file/chunk', method='POST', token=tokenId, body='hello ', params={'uploadId': str(upload['_id'])}, type='text/plain') self.assertStatusOk(resp) # Requesting our offset should work with the token # The offset should not have changed resp = self.request(path='/file/offset', method='GET', token=tokenId, params={'uploadId': upload['_id']}) self.assertStatusOk(resp) self.assertEqual(resp.json['offset'], 6) # Upload the second chunk resp = self.request(path='/file/chunk', method='POST', token=tokenId, body='world', params={ 'offset': 6, 'uploadId': str(upload['_id']) }, type='text/plain') self.assertStatusOk(resp) # Trying to upload more chunks should fail resp = self.request(path='/file/chunk', method='POST', token=tokenId, body='extra', params={ 'offset': 11, 'uploadId': str(upload['_id']) }, type='text/plain') self.assertStatus(resp, 401) # The token should be destroyed self.assertIsNone(Token().load(tokenId, force=True, objectId=False))
def _createStateToken(self, redirect): csrfToken = Token().createToken(days=0.25) # The delimiter is arbitrary, but a dot doesn't need to be URL-encoded state = '%s.%s' % (csrfToken['_id'], redirect) return state