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 }
@@ -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,108 @@ 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+ }
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 ([]);
241347 }
242348}
0 commit comments