def test_validate_aud(self): id_token = jwt.encode({'alg': 'HS256'}, {'aud': 'foo'}, 'k') claims_options = { 'aud': { 'essential': True, 'value': 'foo' } } claims = jwt.decode(id_token, 'k', claims_options=claims_options) claims.validate() claims.options = { 'aud': {'values': ['bar']} } self.assertRaises( errors.InvalidClaimError, claims.validate ) id_token = jwt.encode({'alg': 'HS256'}, {'aud': ['foo', 'bar']}, 'k') claims = jwt.decode(id_token, 'k', claims_options=claims_options) claims.validate() # no validate claims.options = {'aud': {'values': []}} claims.validate()
def authorize(): """ @todo move this into login()? change url redirect on twitch for /login @todo better error handling, redirect to an error page with better troubleshooting/contact admin @todo Cache the keys for X amount of time @todo Better nonce check? could someone pass their own nonce to defeat the condition check? !=? ==? """ # Send user back to login if no nonce is set (shouldn't access /authorize directly) #if not session.get('OIDC_nonce'): # return redirect(url_force('login')) # Make sure we received an auth code before requesting an access token if not request.values.get('code'): return "Invalid response during OpenID exchange with Twitch. (missing auth code)" else: token = oauth.twitch.authorize_access_token() if (token.get('nonce') != session.get('OIDC_nonce')): return "Unexpected response during OpenID exchange with Twitch. (mismatched nonce)" try: # Validate the ID Token by decoding it, exception raised if it fails jwt.decode(token.get('id_token'), twitch.get_oauth_keys()) except jwt.BadSignatureError: return "Unexpected response during OpenID exchange with Twitch. (invalid id token)" claims = oauth.twitch.parse_id_token(token) # Confirm we're the audience for this ID Token if (claims.get('aud') != current_app.config.get('TWITCH_CLIENT_ID')): return "Unexpected response during OpenID exchange with Twitch. (mismatched audience)" # Oof! Pass the information on to the handler return handle_authorize(token, claims)
def test_validate_exp(self): id_token = jwt.encode({'alg': 'HS256'}, {'exp': 'invalid'}, 'k') claims = jwt.decode(id_token, 'k') self.assertRaises(errors.InvalidClaimError, claims.validate) id_token = jwt.encode({'alg': 'HS256'}, {'exp': 1234}, 'k') claims = jwt.decode(id_token, 'k') self.assertRaises(errors.ExpiredTokenError, claims.validate)
def test_validate_nbf(self): id_token = jwt.encode({'alg': 'HS256'}, {'nbf': 'invalid'}, 'k') claims = jwt.decode(id_token, 'k') self.assertRaises(errors.InvalidClaimError, claims.validate) id_token = jwt.encode({'alg': 'HS256'}, {'nbf': 1234}, 'k') claims = jwt.decode(id_token, 'k') claims.validate() id_token = jwt.encode({'alg': 'HS256'}, {'nbf': 1234}, 'k') claims = jwt.decode(id_token, 'k') self.assertRaises(errors.InvalidTokenError, claims.validate, 123)
def decode_auth_token(self, auth_token, jwks=None): """ Decodes the auth token :param auth_token: :return: integer|string """ try: if jwks: payload = jwt.decode(auth_token, jwks) else: payload = jwt.decode(auth_token, self.public_key()) return payload except: LOGGER.exception('token is invalid', exc_info=True) raise Exception('token is invalid')
def valide_token(self, token: str): """validate token string, return a parsed token if valid, return None if not valid :return tuple (is_valid -> bool, id_token or None) """ try: if "https://accounts.google.com" in self.metadata_url: # google's certs would change from time to time, let's refetch it before every try self.fetch_jwk() token = jwt.decode(token, self.jwk) except ValueError as e: if str(e) == 'Invalid JWK kid': logger.info( 'This token cannot be decoded with current provider, will try another provider if available.' ) return None, None else: raise e try: token.validate() return True, token except ExpiredTokenError as e: logger.info('Auth header expired, %s', e) return True, None except JoseError as e: logger.debug('Jose error: %s', e) report() return None, None
def parse_id_token(remote, id_token, claims_options, access_token=None, nonce=None): """Parse UserInfo from id_token.""" def load_key(header, payload): jwk_set = remote.fetch_jwk_set() try: return jwk.loads(jwk_set, header.get('kid')) except ValueError: jwk_set = remote.fetch_jwk_set(force=True) return jwk.loads(jwk_set, header.get('kid')) claims_params = dict( nonce=nonce, client_id=remote.client_id, ) if access_token: claims_params['access_token'] = access_token claims_cls = CodeIDToken else: claims_cls = ImplicitIDToken claims = jwt.decode( id_token, key=load_key, claims_cls=claims_cls, claims_options=claims_options, claims_params=claims_params, ) claims.validate(leeway=120) return UserInfo(claims)
def get_jwt(): """ Get Authorization token and validate its signature against the application's secret key. """ expected_errors = { KeyError: 'Wrong JWT payload structure', TypeError: '<SECRET_KEY> is missing', BadSignatureError: 'Failed to decode JWT with provided key', DecodeError: 'Wrong JWT structure' } token = get_auth_token() try: credentials = jwt.decode(token, current_app.config['SECRET_KEY']) client_id = credentials['client_id'] client_secret = credentials['client_secret'] tenant_id = credentials['tenant_id'] return { 'client_id': client_id, 'client_secret': client_secret, 'tenant_id': tenant_id } except tuple(expected_errors) as error: message = expected_errors[error.__class__] raise AuthorizationError(message)
async def validate_passport(passport: Dict) -> JWTClaims: """Decode a passport and validate its contents.""" LOG.debug("Validating passport.") # Passports from `get_ga4gh_controlled()` will be of form # passport[0] -> encoded passport (JWT) # passport[1] -> unverified decoded header (contains `jku`) # Passports from `get_bona_fide_status()` will be of form # passport[0] -> encoded passport (JWT) # passport[1] -> unverified decoded header (contains `jku`) # passport[2] -> unverified decoded payload # JWT decoding and validation settings # The `aud` claim will be ignored, because Beacon has no prior knowledge # as to where the token has originated from, and is therefore unable to # verify the intended audience. Other claims will be validated as per usual. claims_options = {"aud": {"essential": False}} # Attempt to decode the token and validate its contents # None of the exceptions are fatal, and will not raise an exception # Because even if the validation of one passport fails, the query # Should still continue in case other passports are valid try: # Get JWK for this passport from a third party provider # The JWK will be requested from a URL that is given in the `jku` claim in the header passport_key = await get_jwk(passport[1].get("jku")) # Decode the JWT using public key decoded_passport = jwt.decode(passport[0], passport_key, claims_options=claims_options) # Validate the JWT signature decoded_passport.validate() # Return decoded and validated payload contents return decoded_passport except Exception as e: LOG.error(f"Something went wrong when processing JWT tokens: {e}")
def get_jwt(access_token): from authlib.jose import jwt public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyi7yZQmSCcMjcLxz+/qZe8eTzqcesfWJ71soc80/Bw0ccd9Oftk6HJsdSa86nXPlcbvS/eM2+u5o0MHZNjk0f04sc8KO+U0HWLTrpVP8mcG5VCL51vGlrIvv6Mvb+id0PZxqVP8e+Pp1z1lL7QJLyjWgx6XMr53S9b8Ek2a/TTqaWQTrx/xm7pLsN8UGOqi37vsHt0MOdWqx9Y7K87PIxDFky7z0dxdB0/pDknt7o38cUnQmh46xcuZbBCNu9RmozZ0s4mSU7a4d8qiM41SY8GQoz3NyUpALgdnqZ6jSgnxvZql1sLmQJhMeLYRkkDDbKNQLdxmG3Q4mGDQ1vE1QlwIDAQAB" key = '-----BEGIN PUBLIC KEY-----\n' + public_key + '\n-----END PUBLIC KEY-----' key_binary = key.encode('ascii') claims = jwt.decode(access_token, key_binary) return claims
def test_health_call_success(route, client, avotx_api_request, valid_jwt): app = client.application avotx_api_request.return_value = avotx_api_response(HTTPStatus.OK) response = client.post(route, headers=headers(valid_jwt)) expected_url = f"{app.config['AVOTX_URL']}/api/v1/user/me" expected_headers = { 'User-Agent': app.config['CTR_USER_AGENT'], 'X-OTX-API-KEY': (jwt.decode(valid_jwt, app.config['SECRET_KEY'])['key']), } expected_params = {} avotx_api_request.assert_called_once_with(expected_url, headers=expected_headers, params=expected_params) expected_payload = {'data': {'status': 'ok'}} assert response.status_code == HTTPStatus.OK assert response.get_json() == expected_payload
async def mark_ticket(request): """call ticket op to handle this handler only make authenticate disappear for provider""" # verify jwt for callback url token = request.query_params.get('token') ticket_id = request.path_params['ticket_id'] try: payload = jwt.decode(token, config.SESSION_SECRET_KEY) logger.debug(f'recieved callback req: {payload}') assert payload['ticket_id'] == ticket_id except (jwterrors.BadSignatureError, AssertionError): raise ApiError(ApiErrors.parameter_validation_failed, description="token error") ticket = await Ticket.get(ticket_id) if not ticket: raise ApiError(ApiErrors.not_found, description=f"ticket {ticket_id} not found!") try: data = await request.json() assert 'execution_status' in data, "mark body fields error" ticket.annotate(execution_status=data["execution_status"], final_exec_status=True) logger.debug(f"ticket annotation: {ticket.annotation}") # add notification to ticket mark action await ticket.notify(TicketPhase.MARK) await ticket.save() except (RuntimeError, AssertionError) as e: raise ApiError(ApiErrors.parameter_validation_failed, description=f'decode mark body error: {str(e)}') return dict(msg='Success')
def extract_auth_info(auth_token): try: claims = jwt.decode(auth_token, get_public_key()) except (BadSignatureError, DecodeError): return None return dict(claims)
def _parse_id_token(self, token_data, nonce): id_token = token_data['id_token'] claims_params = { 'nonce': nonce, 'client_id': self.oidc_settings['client_id'] } if 'access_token' in token_data: claims_params['access_token'] = token_data['access_token'] claims_cls = CodeIDToken else: claims_cls = ImplicitIDToken # XXX: should we allow extra claims to be specified in the settings? claims_options = {'iss': {'values': [self.oidc_settings['issuer']]}} claims = jwt.decode( id_token, key=self._load_jwk, claims_cls=claims_cls, claims_options=claims_options, claims_params=claims_params, ) claims.validate(leeway=120) info = UserInfo(claims) for key in INTERNAL_FIELDS: info.pop(key, None) return info
def test_app(context, app): client_id = 'baa448a8-205c-4faa-a048-a10e4b32a136' client_secret = 'joWgziYLap3eKDL6Gk2SnkJoyz0F8ukB' resp = app.post('/auth/token', auth=(client_id, client_secret), data={ 'grant_type': 'client_credentials', 'scope': 'profile', }) assert resp.status_code == 200, resp.text data = resp.json() assert data == { 'token_type': 'Bearer', 'expires_in': 864000, 'scope': 'profile', 'access_token': data['access_token'], } config = context.get('config') key = jwk.loads( json.loads((config.config_path / 'keys/public.json').read_text())) token = jwt.decode(data['access_token'], key) assert token == { 'iss': config.server_url, 'sub': client_id, 'aud': client_id, 'iat': int(token['iat']), 'exp': int(token['exp']), 'scope': 'profile', }
def parse_openid(self, token, nonce=None): def load_key(header, payload): jwk_set = self.fetch_jwk_set() try: return jwk.loads(jwk_set, header.get('kid')) except ValueError: jwk_set = self.fetch_jwk_set(force=True) return jwk.loads(jwk_set, header.get('kid')) claims_options = { 'iss': { 'values': [issuer_url], }, } claims_params = { 'nonce': nonce, 'client_id': self.client_id, } access_token = token.get('access_token') if access_token is not None: claims_params['access_token'] = access_token claims_cls = CodeIDToken else: claims_cls = ImplicitIDToken claims = jwt.decode(token['id_token'], key=load_key, claims_cls=claims_cls, claims_options=claims_options, claims_params=claims_params) claims.validate(leeway=120) return UserInfo(claims)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] not in ("http", "websocket"): # pragma: no cover await self.app(scope, receive, send) return connection = HTTPConnection(scope) initial_session_was_empty = True if self.session_cookie in connection.cookies: data = connection.cookies[self.session_cookie].encode("utf-8") try: jwt_payload = jwt.decode( data, str( self.jwt_secret.decode if self.jwt_secret.decode else self.jwt_secret.encode ), ) jwt_payload.validate_exp(time.time(), 0) scope["session"] = jwt_payload initial_session_was_empty = False except (BadSignatureError, ExpiredTokenError): scope["session"] = {} else: scope["session"] = {} async def send_wrapper(message: Message) -> None: if message["type"] == "http.response.start": if scope["session"]: if "exp" not in scope["session"]: scope["session"]["exp"] = int(time.time()) + self.max_age data = jwt.encode( self.jwt_header, scope["session"], str(self.jwt_secret.encode) ) headers = MutableHeaders(scope=message) header_value = "%s=%s; path=/; Max-Age=%d; %s" % ( self.session_cookie, data.decode("utf-8"), self.max_age, self.security_flags, ) if self.domain: # pragma: no cover header_value += f"; domain={self.domain}" headers.append("Set-Cookie", header_value) elif not initial_session_was_empty: # The session has been cleared. headers = MutableHeaders(scope=message) header_value = "%s=%s; %s" % ( self.session_cookie, "null; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT;", self.security_flags, ) if self.domain: # pragma: no cover header_value += f"; domain={self.domain}" headers.append("Set-Cookie", header_value) await send(message) await self.app(scope, receive, send_wrapper)
def userinfo(): req = flask.request log_request('GET-USERINFO', req) access_token = req.headers.get('Authorization', None) # TODO: if not access_token # TODO: Validate access-token log.info("GET-USERINFO: Access token: '{}'".format(access_token)) access_token_parts = access_token.split() if access_token_parts[0].lower() != 'bearer' or len(access_token_parts) != 2: return flask.render_template('error.html', text='Invalid authorization') access_token = access_token_parts[1] # FIXME with open('jwt-key.pub', 'rb') as f: key_data = f.read() pub_key = JsonWebKey.import_key(key_data, {'kty': 'RSA'}) access_token_json = jwt.decode(access_token, pub_key) scope = access_token_json['scope'] # TODO: Validate audience in access token covers /userinfo log.info("GET-USERINFO: Access token audience: '{}'".format(access_token_json['aud'])) log.info("GET-USERINFO: Scope '{}'".format(scope)) claims = dict() # See https://openid.net/specs/openid-connect-basic-1_0.html#StandardClaims for what claims to include in access token if 'profile' in scope: claims['name'] = 'Name of user is {}'.format(access_token_json['sub'].capitalize()) return claims
def verifyToken(self, accessToken=None, jwks=None): """Verify access token :param str accessToken: access token :param dict jwks: JWKs :return: dict """ # Define an access token if not accessToken: accessToken = self.token["access_token"] # Renew a JWKs of an identity provider if needed if not jwks: result = self.updateJWKs() if not result["OK"]: return result jwks = self.jwks if not jwks: return S_ERROR("JWKs not found.") # Try to decode and verify an access token self.log.debug("Try to decode token %s with JWKs:\n" % accessToken, pprint.pformat(jwks)) try: return S_OK(jwt.decode(accessToken, JsonWebKey.import_key_set(jwks))) except Exception as e: self.log.exception(e) return S_ERROR(repr(e))
def _verify_token(nonce: str, email, raw_token: str, public_key: bytes, claims_cls=None): """Verify a JWT token signature with the public_key.""" claims_options = { "iss": { "essential": True, "value": TOKEN_NAMESPACE }, "aud": { "essential": True, "value": CLIENT_ID }, "sub": { "essential": True, "value": email }, } decoded_token = jwt.decode( raw_token, public_key, claims_cls=claims_cls, claims_options=claims_options, claims_params={"nonce": nonce}, ) decoded_token.validate() return decoded_token
def decode_token(self, token: (JWT, str, dict)): try: decoded_token = jwt.decode(s=token, key=self.key_holder.get_public_key()) except (BadSignatureError, Exception) as err: raise ValidationError(err) return decoded_token
def decode_api_key_v2(api_key: str, auth_public_key: str, required_scopes=None) -> Optional[TokenInfo]: """ Check and retrieve authentication information from api_key. Returned value will be passed in 'token_info' parameter of your operation function, if there is one. 'sub' or 'uid' will be set in 'user' parameter of your operation function, if there is one. Should return None if api_key is invalid or does not allow access to called API. """ try: token = jwt.decode(api_key, key=auth_public_key) except AuthlibBaseError: return None expires_at = datetime.datetime.fromtimestamp(token['exp']) user_info = UserInfo.parse_raw(token['user_info']) now = datetime.datetime.now() if now > expires_at: return None t = TokenInfo( user_id=user_info.user_id, groups_read=[g.group_id for g in user_info.groups], groups_write=[g.group_id for g in user_info.groups if g.mode in (Mode.write, Mode.admin)], groups_admin=[g.group_id for g in user_info.groups if g.mode == Mode.admin], api_key=api_key, auth_method=AuthMethod.bearer ) return t
def validate_passport(passport): """Validate passport claims Accepts: passport(tuple) : ( passport(str), header ) or ( passport(str), header, payload ) """ LOG.info("Validating passport") token = passport[0] header = passport[1] # Beacon can't verify audience because it does not know where the token originated in the first place claims_options = {"aud": {"essential": False}} try: # obtain public key for this passport public_key = elixir_key(header.get("jku")) # Try decoding the token using the public key decoded_passport = jjwt.decode(token, public_key, claims_options=claims_options) # And validating the signature decoded_passport.validate() return decoded_passport except Exception as ex: LOG.error(f"Error while decoding/validating passport:{ex}")
async def validate_token(token: str) -> None: """Validate JWT.""" LOG.debug("Validating access token.") # Get JWK to decode the token with jwk = await get_jwk() # Claims that must exist in the token, and required values if specified claims_options = { "iss": {"essential": True, "values": CONFIG.aai["iss"].split(",")}, # Token allowed from these issuers "aud": {"essential": True, "values": CONFIG.aai["aud"].split(",")}, # Token allowed for these audiences "iat": {"essential": True}, "exp": {"essential": True}, } try: # Decode the token and validate the contents decoded_data = jwt.decode(token, jwk, claims_options=claims_options) decoded_data.validate() except MissingClaimError as e: raise web.HTTPUnauthorized(text=f"Could not validate access token: Missing claim(s): {e}") except ExpiredTokenError as e: raise web.HTTPUnauthorized(text=f"Could not validate access token: Expired signature: {e}") except InvalidClaimError as e: raise web.HTTPForbidden(text=f"Could not validate access token: Token info not corresponding with claim: {e}") except BadSignatureError as e: raise web.HTTPForbidden(text=f"Could not validate access token: Token signature could not be verified: {e}")
def parse_id_token(token_data, nonce): def load_key(header, payload): # TODO: cache this? jwk_set = requests.get(current_app.config['OIDC_JWKS_URL']).json() return jwk.loads(jwk_set, header.get('kid')) id_token = token_data['id_token'] claims_params = { 'nonce': nonce, 'client_id': current_app.config['OIDC_CLIENT_ID'] } if 'access_token' in token_data: claims_params['access_token'] = token_data['access_token'] claims_cls = CodeIDToken else: claims_cls = ImplicitIDToken claims_options = {'iss': {'values': [current_app.config['OIDC_ISSUER']]}} claims = jwt.decode( id_token, key=load_key, claims_cls=claims_cls, claims_options=claims_options, claims_params=claims_params, ) claims.validate(leeway=120) return UserInfo(claims)
def check_authorized(self, headers, path, write=False): if ENABLE_AUTH: try: if not request.headers["Authorization"].startswith("Bearer "): return 400 token = request.headers["Authorization"].split(" ")[1] claims = jwt.decode(token, open(AUTH_TOKEN_PUBKEY).read()) claims.validate() if claims["iss"] != AUTH_TOKEN_ISSUER: return 401 # TODO: Check 'aud' claim matches 'mocks.<domain>' if not self._check_path_match( path, claims["x-nmos-registration"]["read"]): return 403 if write: if not self._check_path_match( path, claims["x-nmos-registration"]["write"]): return 403 except KeyError: # TODO: Add debug which can be returned in the error response JSON return 400 except Exception: # TODO: Add debug which can be returned in the error response JSON return 400 return True
def test_validate_iat(self): id_token = jwt.encode({'alg': 'HS256'}, {'iat': 'invalid'}, 'k') claims = jwt.decode(id_token, 'k') self.assertRaises( errors.InvalidClaimError, claims.validate )
async def __call__( self, request: Request ) -> Tuple[bool, Union[dict, HTTPException]]: authorization: str = request.headers.get("Authorization") scheme, token = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "bearer": token = request.cookies.get(config.JWT_ACCESS_TOKEN_COOKIE) if not token: error = HTTPException( status_code=HTTP_401_UNAUTHORIZED, detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) if self.auto_error: raise error else: return False, error try: token = jwt.decode(token, config.JWT_PUBLIC_KEY) token.validate() except ExpiredTokenError as e: error = HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=str(e)) if self.auto_error: raise error else: return False, error except JoseError as e: error = HTTPException(status_code=HTTP_400_BAD_REQUEST, detail=str(e)) if self.auto_error: raise error else: return False, error return True, token
def validate_request(request): try: authorization_value = request.headers["Authorization"] except KeyError: raise web.HTTPUnprocessableEntity(text="No authorization header provided!") auth_type, auth_token = authorization_value.split(" ") if auth_type != "Bearer": raise web.HTTPUnprocessableEntity(text="Invalid authentication method!") tokens = request.app["tokens"] if auth_token not in tokens.values(): raise web.HTTPUnprocessableEntity(text="Invalidated authentication token used!") claims = jwt.decode(auth_token, request.app["public_key"]) try: claims.validate() return claims except errors.ExpiredTokenError: del request.app["tokens"][get_key_with_value(tokens, auth_token)] raise web.HTTPUnprocessableEntity(text="Provided token is expired!")
def wrapper(*args, **kwargs): response, status_code = func(*args, **kwargs) claims = {} if bool(request) is True: # If bool(request) is False, then we're not responding # to a HTTP request. This happens for instance when we're # updating the cache. if test and mock_jwt_claims is not None: claims = copy.deepcopy(mock_jwt_claims) else: try: jwt_token = request.headers.get('Authorization') jwt_secret = os.environ.get("JWT_SECRET") if jwt_token: # remove type ("Bearer") if present jwt_token = jwt_token.split(" ")[-1] claims = jwt.decode(jwt_token, jwt_secret) claims.validate() except Exception: # This is only to extract claims if the JWT is valid. # If we're unable to get claims from a valid JWT, # we just return an empty dictionary. # We don't throw any exceptions at this point. claims = {} filter_func(response, claims) return response, status_code