Repository: yt-dlp/yt-dlp
File: yt_dlp/extractor/ard.py (Python)

Code before:
            ), {str.strip}),
        }), get_all=False)

    def _real_extract(self, url):
        display_id = self._match_id(url)

        query = {
            'embedded': 'false',
            'mcV6': 'true'
        }
        headers = {}

        ams_cookie = self._get_cookies("https://sso.ardmediathek.de/sso/token").get("ams")
        if ams_cookie:
            token = self._download_json('https://sso.ardmediathek.de/sso/token', display_id,
                                        fatal=False, note='Getting token for age verification')
            id_token = traverse_obj(token, 'idToken')
            if not id_token:
                self.report_warning('Unable to find id token, continuing without authentication')

            else:
                jwt_token_decoded = jwt_decode_hs256(id_token)
                user_id = traverse_obj(jwt_token_decoded, 'user_id', 'sub')

                headers['x-authorization'] = f'Bearer {id_token}'
                query['userId'] = user_id

        page_data = self._download_json(
            f'https://api.ardmediathek.de/page-gateway/pages/ard/item/{display_id}',
            display_id, query=query, headers=headers)

        # For user convenience we use the old contentId instead of the longer crid
        # Ref: https://github.com/yt-dlp/yt-dlp/issues/8731#issuecomment-1874398283
        old_id = traverse_obj(page_data, ('tracking', 'atiCustomVars', 'contentId', {int}))
        if old_id is not None:
            video_id = str(old_id)
            archive_ids = [make_archive_id(ARDBetaMediathekIE, display_id)]
        else:
            self.report_warning(f'Could not extract contentId{bug_reports_message()}')
            video_id = display_id
            archive_ids = None

        player_data = traverse_obj(
            page_data, ('widgets', lambda _, v: v['type'] in ('player_ondemand', 'player_live'), {dict}), get_all=False)
        is_live = player_data.get('type') == 'player_live'
        media_data = traverse_obj(player_data, ('mediaCollection', 'embedded', {dict}))

        if player_data.get('blockedByFsk'):
            self.raise_no_formats(
                'This video is only available after 22:00, '
                'try passing cookies to enable age verification ', expected=True)

Review comment (security):
using the power of traversal, we can check both values before warning/aborting

just to confirm, the `user_id` is found in both the `user_id` and `sub` fields of the JWT, right?
```suggestion
            id_token = traverse_obj(token, ('idToken', {str}))
            user_id = traverse_obj(id_token, ({jwt_decode_hs256}, ('user_id', 'sub'), {str}), get_all=False)
            if not id_token or not user_id:
                self.report_warning('Unable to extract token, continuing without authentication')
            else:
                headers['x-authorization'] = f'Bearer {id_token}'
                query['userId'] = user_id
```

Code after:
                ('title', {str}),
            ), {str.strip}),
        }), get_all=False)

    def _real_extract(self, url):
        display_id = self._match_id(url)
        query = {'embedded': 'false', 'mcV6': 'true'}
        headers = {}

        if self._get_cookies(self._TOKEN_URL).get('ams'):
            token = self._download_json(
                self._TOKEN_URL, display_id, 'Fetching token for age verification',
                'Unable to fetch age verification token', fatal=False)
            id_token = traverse_obj(token, ('idToken', {str}))
            decoded_token = traverse_obj(id_token, ({jwt_decode_hs256}, {dict}))
            user_id = traverse_obj(decoded_token, (('user_id', 'sub'), {str}), get_all=False)
            if not user_id:
                self.report_warning('Unable to extract token, continuing without authentication')
            else:
                headers['x-authorization'] = f'Bearer {id_token}'
                query['userId'] = user_id
                if decoded_token.get('age_rating') != 18:
                    self.report_warning('Account is not verified as 18+; video may be unavailable')

        page_data = self._download_json(
            f'https://api.ardmediathek.de/page-gateway/pages/ard/item/{display_id}',
            display_id, query=query, headers=headers)

        # For user convenience we use the old contentId instead of the longer crid
        # Ref: https://github.com/yt-dlp/yt-dlp/issues/8731#issuecomment-1874398283
        old_id = traverse_obj(page_data, ('tracking', 'atiCustomVars', 'contentId', {int}))
        if old_id is not None:
            video_id = str(old_id)
            archive_ids = [make_archive_id(ARDBetaMediathekIE, display_id)]
        else:
            self.report_warning(f'Could not extract contentId{bug_reports_message()}')
            video_id = display_id
            archive_ids = None

        player_data = traverse_obj(
            page_data, ('widgets', lambda _, v: v['type'] in ('player_ondemand', 'player_live'), {dict}), get_all=False)
        is_live = player_data.get('type') == 'player_live'
        media_data = traverse_obj(player_data, ('mediaCollection', 'embedded', {dict}))

        if player_data.get('blockedByFsk'):
            self.raise_login_required('This video is only available for age verified users or after 22:00')

        formats = []
        subtitles = {}
        for stream in traverse_obj(media_data, ('streams', ..., {dict})):
            kind = stream.get('kind')