2323use OCP \AppFramework \Utility \ITimeFactory ;
2424use OCP \Authentication \Exceptions \ExpiredTokenException ;
2525use OCP \Authentication \Exceptions \InvalidTokenException ;
26+ use OCP \Authentication \Token \IToken ;
2627use OCP \DB \Exception ;
28+ use OCP \GlobalScale \IConfig as GlobalScaleConfig ;
29+ use OCP \GlobalScale \IGlobalScaleService ;
2730use OCP \IDBConnection ;
2831use OCP \IRequest ;
32+ use OCP \IURLGenerator ;
33+ use OCP \IUserManager ;
2934use OCP \Security \Bruteforce \IThrottler ;
3035use OCP \Security \ICrypto ;
3136use OCP \Security \ISecureRandom ;
37+ use Psr \Container \ContainerExceptionInterface ;
38+ use Psr \Container \ContainerInterface ;
3239use Psr \Log \LoggerInterface ;
3340
3441#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT )]
@@ -39,16 +46,20 @@ class OauthApiController extends Controller {
3946 public function __construct (
4047 string $ appName ,
4148 IRequest $ request ,
42- private ICrypto $ crypto ,
43- private AccessTokenMapper $ accessTokenMapper ,
44- private ClientMapper $ clientMapper ,
45- private TokenProvider $ tokenProvider ,
46- private ISecureRandom $ secureRandom ,
47- private ITimeFactory $ time ,
48- private LoggerInterface $ logger ,
49- private IThrottler $ throttler ,
50- private ITimeFactory $ timeFactory ,
51- private IDBConnection $ db ,
49+ private readonly ICrypto $ crypto ,
50+ private readonly AccessTokenMapper $ accessTokenMapper ,
51+ private readonly ClientMapper $ clientMapper ,
52+ private readonly TokenProvider $ tokenProvider ,
53+ private readonly ISecureRandom $ secureRandom ,
54+ private readonly ITimeFactory $ time ,
55+ private readonly LoggerInterface $ logger ,
56+ private readonly IThrottler $ throttler ,
57+ private readonly ITimeFactory $ timeFactory ,
58+ private readonly IDBConnection $ db ,
59+ private readonly GlobalScaleConfig $ globalScaleConfig ,
60+ private readonly IUserManager $ userManager ,
61+ private readonly IURLGenerator $ urlGenerator ,
62+ private readonly ContainerInterface $ container ,
5263 ) {
5364 parent ::__construct ($ appName , $ request );
5465 }
@@ -62,7 +73,7 @@ public function __construct(
6273 * @param ?string $client_id Client ID
6374 * @param ?string $client_secret Client secret
6475 * @throws Exception
65- * @return JSONResponse<Http::STATUS_OK, array{access_token: string, token_type: string, expires_in: int, refresh_token: string, user_id: string}, array{}>|JSONResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>
76+ * @return JSONResponse<Http::STATUS_OK, array{access_token: string, token_type: string, expires_in: int, refresh_token: string, user_id: string, "x.nc-gss.secondary_url"?: ?string }, array{}>|JSONResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>
6677 *
6778 * 200: Token returned
6879 * 400: Getting token is not possible
@@ -211,7 +222,8 @@ public function getToken(
211222 );
212223
213224 // Expiration is in 1 hour again
214- $ appToken ->setExpires ($ this ->time ->getTime () + 3600 );
225+ $ expires = $ this ->time ->getTime () + 3600 ;
226+ $ appToken ->setExpires ($ expires );
215227 $ this ->tokenProvider ->updateToken ($ appToken );
216228
217229 $ this ->db ->commit ();
@@ -228,14 +240,109 @@ public function getToken(
228240
229241 $ this ->throttler ->resetDelay ($ this ->request ->getRemoteAddress (), 'login ' , ['user ' => $ appToken ->getUID ()]);
230242
231- return new JSONResponse (
232- [
233- 'access_token ' => $ newToken ,
234- 'token_type ' => 'Bearer ' ,
235- 'expires_in ' => 3600 ,
236- 'refresh_token ' => $ newCode ,
237- 'user_id ' => $ appToken ->getUID (),
238- ]
239- );
243+ $ data = [
244+ 'access_token ' => $ newToken ,
245+ 'token_type ' => 'Bearer ' ,
246+ 'expires_in ' => 3600 ,
247+ 'refresh_token ' => $ newCode ,
248+ 'user_id ' => $ appToken ->getUID (),
249+ ];
250+
251+ if ($ this ->globalScaleConfig ->isGlobalScaleEnabled () && $ this ->globalScaleConfig ->isPrimary ()) {
252+ // Also make sure the access token is available on the secondary instance
253+ $ data ['x.nc-gss.secondary_url ' ] = $ this ->pushTokenToSecondary ($ appToken , $ newToken , $ expires );
254+ }
255+
256+ return new JSONResponse ($ data );
257+ }
258+
259+ /**
260+ * Push the freshly issued app token to the secondary instance holding the
261+ * user's account, so the OAuth client can use it there directly.
262+ */
263+ private function pushTokenToSecondary (IToken $ appToken , string $ newToken , ?int $ expires ): ?string {
264+ $ user = $ this ->userManager ->get ($ appToken ->getUID ());
265+ if ($ user === null ) {
266+ $ this ->logger ->warning ('could not push oauth token to secondary: unknown user ' , ['uid ' => $ appToken ->getUID ()]);
267+ return null ;
268+ }
269+
270+ try {
271+ /** @var IGlobalScaleService $globalScaleService */
272+ $ globalScaleService = $ this ->container ->get (IGlobalScaleService::class);
273+ } catch (ContainerExceptionInterface $ e ) {
274+ $ this ->logger ->warning ('could not push oauth token to secondary: globalsiteselector is not available ' , ['exception ' => $ e ]);
275+ return null ;
276+ }
277+
278+ try {
279+ return $ globalScaleService ->sendToSecondary ($ user , $ this ->urlGenerator ->linkToRoute ('oauth2.OauthApi.pushToken ' ), [
280+ 'uid ' => $ appToken ->getUID (),
281+ 'loginName ' => $ appToken ->getLoginName (),
282+ 'name ' => $ appToken ->getName (),
283+ 'type ' => $ appToken ->getType (),
284+ 'remember ' => $ appToken ->getRemember (),
285+ 'scope ' => $ appToken ->getScopeAsArray (),
286+ 'expires ' => $ expires ,
287+ 'token ' => $ newToken ,
288+ ]);
289+ } catch (\Exception $ e ) {
290+ $ this ->logger ->warning ('could not push oauth token to secondary ' , ['exception ' => $ e ]);
291+ }
292+ return null ;
293+ }
294+
295+ /**
296+ * Receive an app token pushed from the primary instance, so it can be used
297+ * directly against this (secondary) instance.
298+ */
299+ #[PublicPage]
300+ #[NoCSRFRequired]
301+ #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE )]
302+ #[BruteForceProtection(action: 'oauth2PushToken ' )]
303+ public function pushToken (string $ jwt ): JSONResponse {
304+ if (!$ this ->globalScaleConfig ->isGlobalScaleEnabled () || !$ this ->globalScaleConfig ->isSecondary () || $ jwt === '' ) {
305+ $ response = new JSONResponse ([], Http::STATUS_BAD_REQUEST );
306+ $ response ->throttle ();
307+ return $ response ;
308+ }
309+
310+ try {
311+ /** @var IGlobalScaleService $globalScaleService */
312+ $ globalScaleService = $ this ->container ->get (IGlobalScaleService::class);
313+ } catch (ContainerExceptionInterface $ e ) {
314+ $ this ->logger ->warning ('could not receive oauth token from primary: globalsiteselector is not available ' , ['exception ' => $ e ]);
315+ $ response = new JSONResponse ([], Http::STATUS_BAD_REQUEST );
316+ $ response ->throttle ();
317+ return $ response ;
318+ }
319+
320+ try {
321+ $ decoded = $ globalScaleService ->decodePayload ($ jwt );
322+
323+ $ uid = (string )$ decoded ['uid ' ];
324+ if (!$ this ->userManager ->userExists ($ uid )) {
325+ throw new \InvalidArgumentException ('unknown user: ' . $ uid );
326+ }
327+
328+ $ this ->tokenProvider ->generateToken (
329+ (string )$ decoded ['token ' ],
330+ $ uid ,
331+ (string )$ decoded ['loginName ' ],
332+ null ,
333+ (string )$ decoded ['name ' ],
334+ (int )$ decoded ['type ' ],
335+ (int )$ decoded ['remember ' ],
336+ (array )$ decoded ['scope ' ],
337+ $ decoded ['expires ' ] !== null ? (int )$ decoded ['expires ' ] : null ,
338+ );
339+ } catch (\Exception $ e ) {
340+ $ this ->logger ->warning ('could not create pushed oauth token ' , ['exception ' => $ e ]);
341+ $ response = new JSONResponse ([], Http::STATUS_BAD_REQUEST );
342+ $ response ->throttle ();
343+ return $ response ;
344+ }
345+
346+ return new JSONResponse ([]);
240347 }
241348}
0 commit comments