redirect the logged-in user to the provider consent screen // (callback, ?code&state) -> exchange code, store the per-user token, message the opener // The resulting token is bound to the authenticated remote identity, which fixes syncjob user1. require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; function syncjob_oauth_redirect_uri() { // Must match the redirect URI registered at the provider. return 'https://' . $_SERVER['HTTP_HOST'] . '/syncjob-oauth'; } function syncjob_oauth_finish($ok, $payload) { // Renders a tiny page that hands the result back to the opener modal and closes the popup. header('Content-Type: text/html; charset=utf-8'); $json = json_encode(array_merge(array('type' => 'syncjob_oauth', 'ok' => $ok), $payload)); echo 'OAuth'; exit; } // Must be an authenticated session; users additionally need the syncjobs ACL. if (empty($_SESSION['mailcow_cc_username']) || empty($_SESSION['mailcow_cc_role'])) { header('Location: /'); exit; } if ($_SESSION['mailcow_cc_role'] == 'user' && ($_SESSION['acl']['syncjobs'] ?? '0') != '1') { syncjob_oauth_finish(false, array('error' => 'access_denied')); } $action = isset($_GET['action']) ? $_GET['action'] : ((isset($_GET['code']) || isset($_GET['error'])) ? 'callback' : ''); if ($action == 'start') { $source_id = intval($_GET['source_id'] ?? 0); // get/source enforces visibility for the current session $source = syncjob('get', 'source', array('id' => $source_id, 'with_secret' => true)); if (!$source || $source['auth_type'] != 'XOAUTH2' || $source['oauth_flow'] != 'authorization_code') { syncjob_oauth_finish(false, array('error' => 'source_unavailable')); } $state = bin2hex(random_bytes(16)); $_SESSION['syncjob_oauth2state'] = $state; $_SESSION['syncjob_oauth_source_id'] = $source_id; header('Location: ' . imapsync_source_oauth_authorize_url($source, syncjob_oauth_redirect_uri(), $state)); exit; } // --- callback --- if (isset($_GET['error'])) { syncjob_oauth_finish(false, array('error' => substr((string)$_GET['error'], 0, 200))); } // CSRF: state must match the one stored at 'start' if (empty($_GET['code']) || empty($_GET['state']) || !hash_equals((string)($_SESSION['syncjob_oauth2state'] ?? ''), (string)$_GET['state'])) { syncjob_oauth_finish(false, array('error' => 'invalid_state')); } $source_id = intval($_SESSION['syncjob_oauth_source_id'] ?? 0); unset($_SESSION['syncjob_oauth2state'], $_SESSION['syncjob_oauth_source_id']); $source = syncjob('get', 'source', array('id' => $source_id, 'with_secret' => true)); if (!$source || $source['auth_type'] != 'XOAUTH2' || $source['oauth_flow'] != 'authorization_code') { syncjob_oauth_finish(false, array('error' => 'source_unavailable')); } $token = imapsync_source_token_post($source['oauth_token_endpoint'], array( 'grant_type' => 'authorization_code', 'client_id' => $source['oauth_client_id'], 'client_secret' => $source['oauth_client_secret'], 'code' => $_GET['code'], 'redirect_uri' => syncjob_oauth_redirect_uri(), 'scope' => $source['oauth_scope'], )); if (isset($token['error'])) { syncjob_oauth_finish(false, array('error' => $token['error'])); } $email = imapsync_source_oauth_identity($source, $token); if (!$email) { syncjob_oauth_finish(false, array('error' => 'no_identity')); } imapsync_source_store_user_token($source_id, $email, $token); syncjob_oauth_finish(true, array('source_id' => $source_id, 'user1' => $email));