• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ptondereau / biscuit-sf-bundle / 30813598889

03 Aug 2026 12:26PM UTC coverage: 91.238% (-1.9%) from 93.09%
30813598889

push

github

web-flow
feat(revocation)!: compose revocation stores and align 401s with RFC 6750 (#7)

739 of 842 new or added lines in 27 files covered. (87.77%)

1843 of 2020 relevant lines covered (91.24%)

13.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.56
/src/Command/RevocationCheckCommand.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Biscuit\BiscuitBundle\Command;
6

7
use Biscuit\Auth\UnverifiedBiscuit;
8
use Biscuit\BiscuitBundle\Revocation\Exception\RevocationStoreUnavailableException;
9
use Biscuit\BiscuitBundle\Revocation\RevocationCheckerInterface;
10
use Biscuit\BiscuitBundle\Revocation\RevocationResult;
11
use Biscuit\BiscuitBundle\Token\BiscuitTokenManagerInterface;
12
use Symfony\Component\Console\Attribute\AsCommand;
13
use Symfony\Component\Console\Command\Command;
14
use Symfony\Component\Console\Input\InputArgument;
15
use Symfony\Component\Console\Input\InputInterface;
16
use Symfony\Component\Console\Input\InputOption;
17
use Symfony\Component\Console\Output\OutputInterface;
18
use Symfony\Component\Console\Style\SymfonyStyle;
19
use Throwable;
20

21
#[AsCommand(
22
    name: 'biscuit:revocation:check',
23
    description: 'Check whether a token or identifier is revoked (exit 0 valid, 1 revoked, 2 error)',
24
)]
25
final class RevocationCheckCommand extends Command
26
{
27
    public function __construct(
3 ✔
28
        private readonly ?RevocationCheckerInterface $checker = null,
29
        private readonly ?BiscuitTokenManagerInterface $tokenManager = null,
30
    ) {
31
        parent::__construct();
3 ✔
32
    }
33

34
    protected function configure(): void
3 ✔
35
    {
36
        $this
3 ✔
37
            ->addArgument('token', InputArgument::OPTIONAL, 'The base64-encoded token to check')
3 ✔
38
            ->addOption(
3 ✔
39
                'id',
3 ✔
40
                null,
3 ✔
41
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
3 ✔
42
                'Check raw revocation identifiers instead of a token',
3 ✔
43
            )
3 ✔
44
            ->addOption('verify', null, InputOption::VALUE_NONE, 'Verify the token signature before checking')
3 ✔
45
            ->addOption('explain', null, InputOption::VALUE_NONE, 'Show the verdict of every store consulted');
3 ✔
46
    }
47

48
    protected function execute(InputInterface $input, OutputInterface $output): int
3 ✔
49
    {
50
        $io = new SymfonyStyle($input, $output);
3 ✔
51

52
        if (null === $this->checker) {
3 ✔
NEW
53
            $io->error('Revocation is not enabled. Set biscuit.revocation.enabled and biscuit.revocation.on_unavailable.');
×
54

NEW
55
            return Command::INVALID;
×
56
        }
57

58
        /** @var string|null $tokenString */
59
        $tokenString = $input->getArgument('token');
3 ✔
60
        /** @var list<string> $rawIds */
61
        $rawIds = $input->getOption('id');
3 ✔
62

63
        if (null === $tokenString && [] === $rawIds) {
3 ✔
NEW
64
            $io->error('Provide a token argument or at least one --id option.');
×
65

NEW
66
            return Command::INVALID;
×
67
        }
68

69
        try {
70
            $result = null !== $tokenString
3 ✔
71
                ? $this->checkToken($input, $tokenString)
3 ✔
NEW
72
                : $this->checker->checkIds(array_values(array_filter($rawIds, static fn (string $id): bool => '' !== $id)));
×
NEW
73
        } catch (RevocationStoreUnavailableException $e) {
×
NEW
74
            $io->error('A revocation store is unavailable: ' . $e->getMessage());
×
75

NEW
76
            return Command::INVALID;
×
NEW
77
        } catch (Throwable $e) {
×
NEW
78
            $io->error('Failed to check token: ' . $e->getMessage());
×
79

NEW
80
            return Command::INVALID;
×
81
        }
82

83
        if ((bool) $input->getOption('explain')) {
3 ✔
84
            $this->explain($io, $result);
1 ✔
85
        }
86

87
        if ($result->degraded) {
3 ✔
NEW
88
            $io->warning('At least one store could not answer, so this verdict is incomplete.');
×
89
        }
90

91
        if ($result->isRevoked()) {
3 ✔
92
            $io->error(sprintf('Revoked by "%s": %s', (string) $result->store, (string) $result->revokedId));
2 ✔
93

94
            return Command::FAILURE;
2 ✔
95
        }
96

97
        $io->success('Not revoked.');
1 ✔
98

99
        return Command::SUCCESS;
1 ✔
100
    }
101

102
    private function checkToken(InputInterface $input, string $tokenString): RevocationResult
3 ✔
103
    {
104
        assert(null !== $this->checker);
105

106
        $verify = (bool) $input->getOption('verify');
3 ✔
107

108
        $token = $verify && null !== $this->tokenManager
3 ✔
NEW
109
            ? $this->tokenManager->parse($tokenString)
×
110
            : UnverifiedBiscuit::fromBase64($tokenString);
3 ✔
111

112
        return $this->checker->check($token);
3 ✔
113
    }
114

115
    private function explain(SymfonyStyle $io, RevocationResult $result): void
1 ✔
116
    {
117
        $io->section('Identifiers checked');
1 ✔
118

119
        foreach ($result->checkedIds as $index => $id) {
1 ✔
120
            $io->writeln(sprintf('  %d: %s', $index, $id));
1 ✔
121
        }
122

123
        $io->section('Stores consulted');
1 ✔
124

125
        $rows = [];
1 ✔
126

127
        foreach ($result->outcomes as $outcome) {
1 ✔
128
            $rows[] = [
1 ✔
129
                $outcome->store,
1 ✔
130
                null !== $outcome->error ? 'unavailable' : (null !== $outcome->revokedId ? 'matched' : 'no match'),
1 ✔
131
                $outcome->error ?? ($outcome->revokedId ?? '-'),
1 ✔
132
                sprintf('%.3f ms', $outcome->durationMs),
1 ✔
133
            ];
1 ✔
134
        }
135

136
        if ([] === $rows) {
1 ✔
NEW
137
            $io->writeln('<comment>No store was consulted.</comment>');
×
138

NEW
139
            return;
×
140
        }
141

142
        $io->table(['Store', 'Result', 'Detail', 'Time'], $rows);
1 ✔
143
    }
144
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc