|JSONResponse * * 200: Token returned * 400: Getting token is not possible */ #[PublicPage] #[NoCSRFRequired] #[BruteForceProtection(action: 'oauth2GetToken')] public function getToken( string $grant_type, ?string $code, ?string $refresh_token, ?string $client_id, ?string $client_secret ): JSONResponse { // We only handle two types if ($grant_type !== 'authorization_code' && $grant_type !== 'refresh_token') { $response = new JSONResponse([ 'error' => 'invalid_grant', ], Http::STATUS_BAD_REQUEST); $response->throttle(['invalid_grant' => $grant_type]); return $response; } // We handle the initial and refresh tokens the same way if ($grant_type === 'refresh_token') { $code = $refresh_token; } try { $accessToken = $this->accessTokenMapper->getByCode($code); } catch (AccessTokenNotFoundException $e) { $response = new JSONResponse([ 'error' => 'invalid_request', ], Http::STATUS_BAD_REQUEST); $response->throttle(['invalid_request' => 'token not found', 'code' => $code]); return $response; } if ($grant_type === 'authorization_code') { // check this token is in authorization code state $deliveredTokenCount = $accessToken->getTokenCount(); if ($deliveredTokenCount > 0) { $response = new JSONResponse([ 'error' => 'invalid_request', ], Http::STATUS_BAD_REQUEST); $response->throttle(['invalid_request' => 'authorization_code_received_for_active_token']); return $response; } // check authorization code expiration $now = $this->timeFactory->now()->getTimestamp(); $codeCreatedAt = $accessToken->getCodeCreatedAt(); if ($codeCreatedAt < $now - self::AUTHORIZATION_CODE_EXPIRES_AFTER) { // we know this token is not useful anymore $this->accessTokenMapper->delete($accessToken); $response = new JSONResponse([ 'error' => 'invalid_request', ], Http::STATUS_BAD_REQUEST); $expiredSince = $now - self::AUTHORIZATION_CODE_EXPIRES_AFTER - $codeCreatedAt; $response->throttle(['invalid_request' => 'authorization_code_expired', 'expired_since' => $expiredSince]); return $response; } } try { $client = $this->clientMapper->getByUid($accessToken->getClientId()); } catch (ClientNotFoundException $e) { $response = new JSONResponse([ 'error' => 'invalid_request', ], Http::STATUS_BAD_REQUEST); $response->throttle(['invalid_request' => 'client not found', 'client_id' => $accessToken->getClientId()]); return $response; } if (isset($this->request->server['PHP_AUTH_USER'])) { $client_id = $this->request->server['PHP_AUTH_USER']; $client_secret = $this->request->server['PHP_AUTH_PW']; } try { $storedClientSecretHash = $client->getSecret(); $clientSecretHash = bin2hex($this->crypto->calculateHMAC($client_secret)); } catch (\Exception $e) { $this->logger->error('OAuth client secret decryption error', ['exception' => $e]); // we don't throttle here because it might not be a bruteforce attack return new JSONResponse([ 'error' => 'invalid_client', ], Http::STATUS_BAD_REQUEST); } // The client id and secret must match. Else we don't provide an access token! if ($client->getClientIdentifier() !== $client_id || $storedClientSecretHash !== $clientSecretHash) { $response = new JSONResponse([ 'error' => 'invalid_client', ], Http::STATUS_BAD_REQUEST); $response->throttle(['invalid_client' => 'client ID or secret does not match']); return $response; } $decryptedToken = $this->crypto->decrypt($accessToken->getEncryptedToken(), $code); // Obtain the appToken associated try { $appToken = $this->tokenProvider->getTokenById($accessToken->getTokenId()); } catch (ExpiredTokenException $e) { $appToken = $e->getToken(); } catch (InvalidTokenException $e) { //We can't do anything... $this->accessTokenMapper->delete($accessToken); $response = new JSONResponse([ 'error' => 'invalid_request', ], Http::STATUS_BAD_REQUEST); $response->throttle(['invalid_request' => 'token is invalid']); return $response; } // Rotate the apptoken (so the old one becomes invalid basically) $newToken = $this->secureRandom->generate(72, ISecureRandom::CHAR_ALPHANUMERIC); $appToken = $this->tokenProvider->rotate( $appToken, $decryptedToken, $newToken ); // Expiration is in 1 hour again $appToken->setExpires($this->time->getTime() + 3600); $this->tokenProvider->updateToken($appToken); // Generate a new refresh token and encrypt the new apptoken in the DB $newCode = $this->secureRandom->generate(128, ISecureRandom::CHAR_ALPHANUMERIC); $accessToken->setHashedCode(hash('sha512', $newCode)); $accessToken->setEncryptedToken($this->crypto->encrypt($newToken, $newCode)); // increase the number of delivered oauth token // this helps with cleaning up DB access token when authorization code has expired // and it never delivered any oauth token $tokenCount = $accessToken->getTokenCount(); $accessToken->setTokenCount($tokenCount + 1); $this->accessTokenMapper->update($accessToken); $this->throttler->resetDelay($this->request->getRemoteAddress(), 'login', ['user' => $appToken->getUID()]); return new JSONResponse( [ 'access_token' => $newToken, 'token_type' => 'Bearer', 'expires_in' => 3600, 'refresh_token' => $newCode, 'user_id' => $appToken->getUID(), ] ); } }