Skip to content

Commit 247c874

Browse files
authored
Merge pull request #38773 from nextcloud/fix/noid/protect-oauth2-api-controller
Add bruteforce protection in OauthApiController
2 parents ceee417 + 629adc3 commit 247c874

File tree

2 files changed

+23
-5
lines changed

2 files changed

+23
-5
lines changed

apps/oauth2/lib/Controller/OauthApiController.php

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public function __construct(
6464
/**
6565
* @PublicPage
6666
* @NoCSRFRequired
67+
* @BruteForceProtection(action=oauth2GetToken)
6768
*
6869
* @param string $grant_type
6970
* @param string $code
@@ -76,9 +77,11 @@ public function getToken($grant_type, $code, $refresh_token, $client_id, $client
7677

7778
// We only handle two types
7879
if ($grant_type !== 'authorization_code' && $grant_type !== 'refresh_token') {
79-
return new JSONResponse([
80+
$response = new JSONResponse([
8081
'error' => 'invalid_grant',
8182
], Http::STATUS_BAD_REQUEST);
83+
$response->throttle(['invalid_grant' => $grant_type]);
84+
return $response;
8285
}
8386

8487
// We handle the initial and refresh tokens the same way
@@ -89,17 +92,21 @@ public function getToken($grant_type, $code, $refresh_token, $client_id, $client
8992
try {
9093
$accessToken = $this->accessTokenMapper->getByCode($code);
9194
} catch (AccessTokenNotFoundException $e) {
92-
return new JSONResponse([
95+
$response = new JSONResponse([
9396
'error' => 'invalid_request',
9497
], Http::STATUS_BAD_REQUEST);
98+
$response->throttle(['invalid_request' => 'token not found', 'code' => $code]);
99+
return $response;
95100
}
96101

97102
try {
98103
$client = $this->clientMapper->getByUid($accessToken->getClientId());
99104
} catch (ClientNotFoundException $e) {
100-
return new JSONResponse([
105+
$response = new JSONResponse([
101106
'error' => 'invalid_request',
102107
], Http::STATUS_BAD_REQUEST);
108+
$response->throttle(['invalid_request' => 'client not found', 'client_id' => $accessToken->getClientId()]);
109+
return $response;
103110
}
104111

105112
if (isset($this->request->server['PHP_AUTH_USER'])) {
@@ -111,15 +118,18 @@ public function getToken($grant_type, $code, $refresh_token, $client_id, $client
111118
$storedClientSecret = $this->crypto->decrypt($client->getSecret());
112119
} catch (\Exception $e) {
113120
$this->logger->error('OAuth client secret decryption error', ['exception' => $e]);
121+
// we don't throttle here because it might not be a bruteforce attack
114122
return new JSONResponse([
115123
'error' => 'invalid_client',
116124
], Http::STATUS_BAD_REQUEST);
117125
}
118126
// The client id and secret must match. Else we don't provide an access token!
119127
if ($client->getClientIdentifier() !== $client_id || $storedClientSecret !== $client_secret) {
120-
return new JSONResponse([
128+
$response = new JSONResponse([
121129
'error' => 'invalid_client',
122130
], Http::STATUS_BAD_REQUEST);
131+
$response->throttle(['invalid_client' => 'client ID or secret does not match']);
132+
return $response;
123133
}
124134

125135
$decryptedToken = $this->crypto->decrypt($accessToken->getEncryptedToken(), $code);
@@ -132,9 +142,11 @@ public function getToken($grant_type, $code, $refresh_token, $client_id, $client
132142
} catch (InvalidTokenException $e) {
133143
//We can't do anything...
134144
$this->accessTokenMapper->delete($accessToken);
135-
return new JSONResponse([
145+
$response = new JSONResponse([
136146
'error' => 'invalid_request',
137147
], Http::STATUS_BAD_REQUEST);
148+
$response->throttle(['invalid_request' => 'token is invalid']);
149+
return $response;
138150
}
139151

140152
// Rotate the apptoken (so the old one becomes invalid basically)

apps/oauth2/tests/Controller/OauthApiControllerTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ public function testGetTokenInvalidGrantType() {
104104
$expected = new JSONResponse([
105105
'error' => 'invalid_grant',
106106
], Http::STATUS_BAD_REQUEST);
107+
$expected->throttle(['invalid_grant' => 'foo']);
107108

108109
$this->assertEquals($expected, $this->oauthApiController->getToken('foo', null, null, null, null));
109110
}
@@ -112,6 +113,7 @@ public function testGetTokenInvalidCode() {
112113
$expected = new JSONResponse([
113114
'error' => 'invalid_request',
114115
], Http::STATUS_BAD_REQUEST);
116+
$expected->throttle(['invalid_request' => 'token not found', 'code' => 'invalidcode']);
115117

116118
$this->accessTokenMapper->method('getByCode')
117119
->with('invalidcode')
@@ -124,6 +126,7 @@ public function testGetTokenInvalidRefreshToken() {
124126
$expected = new JSONResponse([
125127
'error' => 'invalid_request',
126128
], Http::STATUS_BAD_REQUEST);
129+
$expected->throttle(['invalid_request' => 'token not found', 'code' => 'invalidrefresh']);
127130

128131
$this->accessTokenMapper->method('getByCode')
129132
->with('invalidrefresh')
@@ -136,6 +139,7 @@ public function testGetTokenClientDoesNotExist() {
136139
$expected = new JSONResponse([
137140
'error' => 'invalid_request',
138141
], Http::STATUS_BAD_REQUEST);
142+
$expected->throttle(['invalid_request' => 'client not found', 'client_id' => 42]);
139143

140144
$accessToken = new AccessToken();
141145
$accessToken->setClientId(42);
@@ -169,6 +173,7 @@ public function testGetTokenInvalidClient($clientId, $clientSecret) {
169173
$expected = new JSONResponse([
170174
'error' => 'invalid_client',
171175
], Http::STATUS_BAD_REQUEST);
176+
$expected->throttle(['invalid_client' => 'client ID or secret does not match']);
172177

173178
$accessToken = new AccessToken();
174179
$accessToken->setClientId(42);
@@ -191,6 +196,7 @@ public function testGetTokenInvalidAppToken() {
191196
$expected = new JSONResponse([
192197
'error' => 'invalid_request',
193198
], Http::STATUS_BAD_REQUEST);
199+
$expected->throttle(['invalid_request' => 'token is invalid']);
194200

195201
$accessToken = new AccessToken();
196202
$accessToken->setClientId(42);

0 commit comments

Comments
 (0)