def _respond(_uid, _description): token = crypt.generate_auth_token(_uid) if target == 'dcos:authenticationresponse:html': # Upon entering the SSO flow, the user-agent stored this target to # indicate that it desires to retrieve a special, human-readable # HTML authentication response. generate_authtoken_html_response(token, resp) else: generate_authtoken_json_response(token, req, resp, _uid, _description, is_remote=True) # Perform the redirect. if target is None: # `target` was not defined by user-agent while entering the # SSO flow. Redirect to root. raise falcon.HTTPSeeOther('/') else: # `target` was defined by user-agent upon entering the flow. # Redirect to it w/o further validation (although it may not # even be a valid URL). raise falcon.HTTPSeeOther(target)
def _login_service(self, req, resp, user, login_params): self.log.debug('Service login token: `%s`', login_params.service_login_token) service_pubkey = user.pubkey self.log.info("Service login: validate service login JWT using " "the service's public key") try: payload = jwt.decode(jwt=login_params.service_login_token, key=service_pubkey, algorithms='RS256') except (jwt.InvalidTokenError, ValueError) as e: self.log.info('Service login for `%s`: invalid token `%s`: %s', login_params.uid, login_params.service_login_token, e) self._raise_local_nonauth_error() if 'uid' not in payload: self.log.info('Service login: token misses `uid` claim') self._raise_local_nonauth_error() if payload['uid'] != login_params.uid: self.log.info('Service login: `uid` claim mismatch') self._raise_local_nonauth_error() # Emit warning log messages about insecurely constructed login tokens. if 'exp' not in payload: self.log.warning( 'long-lived service login token (no exp claim) for uid `%s`', login_params.uid) elif payload['exp'] > time.time() + 600: self.log.warning( 'long-lived service login token (> 10 minutes) for uid `%s`', login_params.uid) # In the special case of 'service user accounts', interpret `exp` # parameter (token expiration time) from request, if given (this is a # private interface, must not be used by relying parties. See crypt.py # for more commentary. authtoken = crypt.generate_auth_token(login_params.uid, login_params.exp) generate_authtoken_json_response(authtoken, req, resp, login_params.uid, user.description)
def _login_local_regular_user(self, req, resp, user, login_params): pw_hashed = user.passwordhash self.log.debug('User login: Validate password.') if not crypt.verify_password(login_params.pw, pw_hashed): self.log.info( str( SecurityEventAuditLogEntry( req, { 'action': 'password-login', 'result': 'deny', 'reason': 'invalid password provided', 'uid': login_params.uid, }))) self._raise_local_nonauth_error() authtoken = crypt.generate_auth_token(login_params.uid) generate_authtoken_json_response(authtoken, req, resp, login_params.uid, user.description)
def _oidc_id_token_login(self, req, resp, oidc_id_token): # Hand off the ID Token validation business logic to the # `oidcidtokenlogin` module. issuer, email = oidcidtokenlogin.verify_id_token_or_terminate( req, resp, oidc_id_token) uid = sanitize_remote_uid(email) regular_user_count = dbsession.query(User).filter_by( utype=UserType.regular).count() if regular_user_count == 0: log.info('There is no regular user account yet. Create one.') # Add user to database. Rely on that we have just checked that no # user is there, i.e. a conflict is unexpected. Technically, there # is race condition and if a separate party was faster adding the # same user, `import_remote_user()` below could raise # `bouncer.app.exceptions.EntityExists`. In practice, that requires # the same user to log in multiple times via the external login # method on a sub-second timescale through different Bouncer # instances. Leave this unhandled (one request will succeed, the # others will see a 500 Internal Server Error response). Store # issuer as provider_id so that we keep record of which identity # provider precisely emitted the data. descr = f'User added through OIDC ID Token login. Issuer: {issuer}' user = import_remote_user(uid=uid, description=descr, provider_type=ProviderType.oidc, provider_id=issuer) else: try: user = User.get(uid) except bouncer.app.exceptions.EntityNotFound: log.info( "I know %s user(s), but `%s` ain't one of them. Emit 401.", regular_user_count, uid) # Note(JP): 403 is more appropriate because this is effectively # our coarse-grained authorization mechanism hitting in, but 401 # I think should be maintained for legacy reasons. raise falcon.HTTPUnauthorized( description='ID Token login failed: user unknown', ) # Make sure that provider ID and type are matching. That is if a # user is known in the database with the same uid as presented by # the current ID Token but stemming from a different provider type # or from a different issuer than recorded in the database then # reject the login request. if user.provider_type != ProviderType.oidc: raise falcon.HTTPUnauthorized( description='ID Token login failed: provider type mismatch', ) if user.provider_id != issuer: raise falcon.HTTPUnauthorized( description='ID Token login failed: provider ID mismatch', ) generate_authtoken_json_response(crypt.generate_auth_token(user.uid), req, resp, user.uid, user.description)