Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.35% covered (warning)
54.35%
25 / 46
42.86% covered (danger)
42.86%
6 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
ClientEntity
54.35% covered (warning)
54.35%
25 / 46
42.86% covered (danger)
42.86%
6 / 14
68.05
0.00% covered (danger)
0.00%
0 / 1
 getRedirectUri
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isConfidential
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIdentifier
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setIdentifier
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedGrants
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getScopes
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validate
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getOAuthVersion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isSecretValid
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isGrantAllowed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 authorize
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getOwnerOnlyAccessToken
72.73% covered (warning)
72.73%
16 / 22
0.00% covered (danger)
0.00%
0 / 1
5.51
 getVerifiedScopes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\OAuth\Entity;
4
5use DateInterval;
6use DateTimeImmutable;
7use Exception;
8use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
9use League\OAuth2\Server\Entities\ScopeEntityInterface;
10use League\OAuth2\Server\Exception\OAuthServerException;
11use MediaWiki\Extension\OAuth\Backend\Consumer;
12use MediaWiki\Extension\OAuth\Backend\ConsumerAcceptance;
13use MediaWiki\Extension\OAuth\Backend\MWOAuthException;
14use MediaWiki\Extension\OAuth\Backend\Utils;
15use MediaWiki\Extension\OAuth\Repository\AccessTokenRepository;
16use MediaWiki\Extension\OAuth\Repository\ClaimStore;
17use MediaWiki\User\User;
18
19class ClientEntity extends Consumer implements MWClientEntityInterface {
20
21    /**
22     * Returns the registered redirect URI (as a string).
23     *
24     * Alternatively, return an indexed array of redirect URIs.
25     *
26     * @return string|string[]
27     */
28    public function getRedirectUri(): string|array {
29        return $this->getCallbackUrl();
30    }
31
32    /**
33     * Returns true if the client is confidential.
34     */
35    public function isConfidential(): bool {
36        return $this->oauth2IsConfidential;
37    }
38
39    public function getIdentifier(): string {
40        return $this->getConsumerKey();
41    }
42
43    /**
44     * @param mixed $identifier
45     */
46    public function setIdentifier( $identifier ) {
47        $this->consumerKey = $identifier;
48    }
49
50    /**
51     * Get the grant types this client is allowed to use
52     *
53     * @return string[]
54     */
55    public function getAllowedGrants() {
56        return $this->oauth2GrantTypes;
57    }
58
59    /**
60     * Convenience function, same as getGrants()
61     * it just returns array of ScopeEntity-es instead of strings
62     *
63     * @return ScopeEntityInterface[]
64     */
65    public function getScopes() {
66        $scopeEntities = [];
67        foreach ( $this->getGrants() as $grant ) {
68            $scopeEntities[] = new ScopeEntity( $grant );
69        }
70
71        return $scopeEntities;
72    }
73
74    /**
75     * @return bool|User
76     */
77    public function getUser() {
78        return Utils::getLocalUserFromCentralId( $this->getUserId() );
79    }
80
81    /**
82     * @param null|string $secret
83     * @param null|string $grantType
84     * @return bool
85     */
86    public function validate( $secret, $grantType ) {
87        if ( !$this->isSecretValid( $secret ) ) {
88            return false;
89        }
90
91        if ( !$this->isGrantAllowed( $grantType ) ) {
92            return false;
93        }
94
95        return true;
96    }
97
98    public function getOAuthVersion(): int {
99        return static::OAUTH_VERSION_2;
100    }
101
102    /**
103     * @param null|string $secret
104     * @return bool
105     */
106    private function isSecretValid( $secret ): bool {
107        return is_string( $secret )
108            && hash_equals( $secret, Utils::hmacDBSecret( $this->secretKey ) );
109    }
110
111    public function isGrantAllowed( string $grantType ): bool {
112        return in_array( $grantType, $this->getAllowedGrants() );
113    }
114
115    /**
116     * @param User $mwUser
117     * @param bool $update
118     * @param string[] $grants
119     * @param null $requestTokenKey
120     * @return bool
121     * @throws MWOAuthException
122     */
123    public function authorize( User $mwUser, $update, $grants, $requestTokenKey = null ) {
124        $this->conductAuthorizationChecks( $mwUser );
125
126        $grants = $this->getVerifiedScopes( $grants );
127        $this->saveAuthorization( $mwUser, $update, $grants );
128
129        return true;
130    }
131
132    /**
133     * Get the access token to be used with a single user
134     * Should never be called outside of client registration/manage code
135     *
136     * @param ConsumerAcceptance $approval
137     * @param bool $revokeExisting Delete all existing tokens
138     *
139     * @return AccessTokenEntityInterface
140     * @throws MWOAuthException
141     * @throws OAuthServerException
142     * @throws Exception
143     */
144    public function getOwnerOnlyAccessToken(
145        ConsumerAcceptance $approval, $revokeExisting = false
146    ) {
147        $grantType = 'client_credentials';
148        if (
149            count( $this->getAllowedGrants() ) !== 1 ||
150            $this->getAllowedGrants()[0] !== $grantType
151        ) {
152            // make sure client is allowed *only* client_credentials grant,
153            // so that this AT cannot be used in other grant type requests
154            throw new MWOAuthException( 'mwoauth-oauth2-error-owner-only-invalid-grant', [
155                'consumer' => $this->getConsumerKey(),
156                'consumer_name' => $this->getName(),
157            ] );
158        }
159        $accessTokenRepo = new AccessTokenRepository();
160        if ( $revokeExisting ) {
161            $accessTokenRepo->deleteForApprovalId( $approval->getId() );
162        }
163        /** @var AccessTokenEntity $accessToken */
164        $accessToken = $accessTokenRepo->getNewToken( $this, $this->getScopes(), (string)$approval->getUserId() );
165        '@phan-var AccessTokenEntity $accessToken';
166        $claimStore = new ClaimStore();
167        $claims = $claimStore->getClaims( $grantType, $this, ownerOnly: true );
168        foreach ( $claims as $claim ) {
169            $accessToken->addClaim( $claim );
170        }
171        $accessToken->setExpiryDateTime( ( new DateTimeImmutable() )->add(
172            new DateInterval( 'P1000Y' )
173        ) );
174        $accessToken->setPrivateKeyFromConfig();
175        $accessToken->setIdentifier( bin2hex( random_bytes( 40 ) ) );
176
177        $accessTokenRepo->persistNewAccessToken( $accessToken );
178
179        return $accessToken;
180    }
181
182    /**
183     * Filter out scopes that application cannot use
184     *
185     * @param string[] $requested
186     * @return string[]
187     */
188    private function getVerifiedScopes( $requested ) {
189        return array_intersect( $requested, $this->getGrants() );
190    }
191}