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