online.git: 3 commits - browser/html browser/js browser/src common/Authorization.cpp common/Authorization.hpp kit/ChildSession.cpp test/RequestDetailsTests.cpp wsd/ClientSession.cpp wsd/protocol.txt

0 views
Skip to first unread message

"Ashod Nakashian (via cogerrit)"

unread,
5:23 AM (1 hour ago) 5:23 AM
to collaboraon...@googlegroups.com
browser/html/framed.doc.html | 11 ++-
browser/js/global.js | 2
browser/src/app/Socket.ts | 4 -
browser/src/map/handler/Map.WOPI.js | 3
common/Authorization.cpp | 25 +++++++
common/Authorization.hpp | 62 +++++++++++++++++-
kit/ChildSession.cpp | 1
test/RequestDetailsTests.cpp | 119 ++++++++++++++++++++++++++++++++++++
wsd/ClientSession.cpp | 14 +++-
wsd/protocol.txt | 3
10 files changed, 226 insertions(+), 18 deletions(-)

New commits:
commit 8bbc2e36c9495893f3d7cf59e3366226179b9a95
Author: Ashod Nakashian <ashod.n...@collabora.co.uk>
AuthorDate: Sun Apr 19 11:31:05 2026 -0400
Commit: Ashod Nakashian <ashod.n...@collabora.co.uk>
CommitDate: Wed Apr 29 09:23:40 2026 +0000

wsd: Authorization supports refreshing tokens

With tests.

Change-Id: I37debc0123e46e582d6506c57326405cdb22d946
Signed-off-by: Ashod Nakashian <ashod.n...@collabora.co.uk>
Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/1708
Tested-by: Caolán McNamara <caolan....@collabora.com>
Reviewed-by: Caolán McNamara <caolan....@collabora.com>
Tested-by: Jenkins CPCI <rel...@collaboraoffice.com>

diff --git a/common/Authorization.cpp b/common/Authorization.cpp
index 3dbcdc3b287d..d4adba8273a2 100644
--- a/common/Authorization.cpp
+++ b/common/Authorization.cpp
@@ -131,7 +131,19 @@ Authorization Authorization::create(const Poco::URI& uri)
if (!decoded.empty())
{
Authorization auth(type, std::move(decoded), noHeader);
- auth.setExpiryEpoch(expiryEpoch);
+ if (expiryEpoch > duration::zero())
+ {
+ if (type != Authorization::Type::Token)
+ {
+ LOG_WRN("Ignoring invalid access_token_ttl with ["
+ << name(type) << "] authorization type in uri [" << uri.toString() << ']');
+ }
+ else
+ {
+ auth.setExpiryEpoch(expiryEpoch);
+ }
+ }
+
return auth;
}

diff --git a/common/Authorization.hpp b/common/Authorization.hpp
index 17ddd170b043..386b977b617d 100644
--- a/common/Authorization.hpp
+++ b/common/Authorization.hpp
@@ -13,6 +13,7 @@

#pragma once

+#include <common/Log.hpp>
#include <common/StateEnum.hpp>

#include <chrono>
@@ -39,6 +40,7 @@ public:
None, ///< Unlike Expired, this implies no Authorization needed.
Token, ///< Valid access_token -> "Authorization: Bearer ..." header.
Header, ///< Valid access_header -> Custom header(s).
+ TokenRefresh, ///< Pending a Token refresh from integration.
Expired ///< The server is rejecting the current authorization key.
);

@@ -46,6 +48,8 @@ private:
std::string _data;
Type _type;
duration _expiryEpoch; ///< Milliseconds from the epoch when the access_token will expire.
+ std::chrono::steady_clock::time_point _tokenRefreshStartTime; ///< Only when refreshing.
+ std::chrono::seconds _tokenRefreshTimeout; ///< Maximum time to wait for Token refresh.
bool _noHeader;

Authorization()
@@ -58,6 +62,8 @@ public:
: _data(std::move(data))
, _type(type)
, _expiryEpoch(duration::zero())
+ , _tokenRefreshStartTime(duration::zero())
+ , _tokenRefreshTimeout(std::chrono::seconds::zero())
, _noHeader(noHeader)
{
}
@@ -74,12 +80,46 @@ public:
_expiryEpoch = expiryEpoch;
}

+ /// Returns true iff Type is Token and we passed the expiry-epoch, or
+ /// we are refreshing already.
+ bool needTokenRefresh() const
+ {
+ return _type == Type::TokenRefresh ||
+ (_type == Type::Token && _expiryEpoch > duration::zero() &&
+ std::chrono::system_clock::now().time_since_epoch() > _expiryEpoch);
+ }
+
+ /// Start waiting for a token refresh.
+ void startTokenRefresh(const std::chrono::seconds timeout)
+ {
+ LOG_ASSERT_MSG(_type == Type::Token, "Token refresh is meaningful only for access_token");
+ _type = Type::TokenRefresh;
+ _tokenRefreshStartTime = std::chrono::steady_clock::now();
+ _tokenRefreshTimeout = timeout;
+ }
+
+ /// Returns true iff we are refreshing the token.
+ bool isRefreshingToken() const { return _type == Type::TokenRefresh; }
+
+ /// Returns true if the timeout has elapsed without a refresh.
+ bool isTokenRefreshTimedOut(
+ const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) const
+ {
+ return isRefreshingToken() && (now - _tokenRefreshStartTime) >= _tokenRefreshTimeout;
+ }
+
+ /// Sets the Token's expiry time from the epoch.
+ void setExpiryEpoch(duration epochMs)
+ {
+ LOG_ASSERT_MSG(epochMs == duration::zero() || _type == Type::Token,
+ "Token expiry is meaningful only for access_token");
+ _expiryEpoch = epochMs;
+ }
+
/// Expire the Authorization data.
void expire() { _type = Type::Expired; }

- void setExpiryEpoch(duration epochMs) { _expiryEpoch = epochMs; }
-
- /// Returns true iff the Authorization data is invalid.
+ /// Returns true if Type is Expired or we passed the expiry-epoch.
bool isExpired() const
{
return _type == Type::Expired ||
diff --git a/test/RequestDetailsTests.cpp b/test/RequestDetailsTests.cpp
index bdeefbaa6726..afb7ef50f734 100644
--- a/test/RequestDetailsTests.cpp
+++ b/test/RequestDetailsTests.cpp
@@ -38,6 +38,7 @@ class RequestDetailsTests : public CPPUNIT_NS::TestFixture
CPPUNIT_TEST(testRequestDetails);
CPPUNIT_TEST(testCoolWs);
CPPUNIT_TEST(testAuthorization);
+ CPPUNIT_TEST(testAuthorizationExpiry);
CPPUNIT_TEST(testSanitizePercent);

CPPUNIT_TEST_SUITE_END();
@@ -49,6 +50,7 @@ class RequestDetailsTests : public CPPUNIT_NS::TestFixture
void testRequestDetails();
void testCoolWs();
void testAuthorization();
+ void testAuthorizationExpiry();
void testSanitizePercent();
};

@@ -1349,6 +1351,123 @@ void RequestDetailsTests::testAuthorization()
}
}

+void RequestDetailsTests::testAuthorizationExpiry()
+{
+ constexpr std::string_view testname = __func__;
+
+ using duration = std::chrono::milliseconds;
+
+ // A token with no expiry should not be expired.
+ {
+ Authorization auth(Authorization::Type::Token, "tok1", false);
+ LOK_ASSERT(!auth.isExpired());
+ LOK_ASSERT(!auth.needTokenRefresh());
+ }
+
+ // A token with a far-future expiry should not be expired.
+ {
+ Authorization auth(Authorization::Type::Token, "tok2", false);
+ const auto futureMs = std::chrono::system_clock::now().time_since_epoch() +
+ std::chrono::hours(1);
+ auth.setExpiryEpoch(std::chrono::duration_cast<duration>(futureMs));
+ LOK_ASSERT(!auth.isExpired());
+ LOK_ASSERT(!auth.needTokenRefresh());
+ }
+
+ // A token with a past expiry should be expired.
+ {
+ Authorization auth(Authorization::Type::Token, "tok3", false);
+ const auto pastMs = std::chrono::system_clock::now().time_since_epoch() -
+ std::chrono::seconds(1);
+ auth.setExpiryEpoch(std::chrono::duration_cast<duration>(pastMs));
+ LOK_ASSERT(auth.isExpired());
+ LOK_ASSERT(auth.needTokenRefresh());
+ // Regression: a naturally-expired Token (startTokenRefresh never called) must
+ // not report a refresh-wait timeout, otherwise the poll-loop kills the session.
+ LOK_ASSERT(!auth.isRefreshingToken());
+ LOK_ASSERT(!auth.isTokenRefreshTimedOut());
+ }
+
+ // expire() should mark as expired regardless of TTL.
+ {
+ Authorization auth(Authorization::Type::Token, "tok4", false);
+ LOK_ASSERT(!auth.isExpired());
+ auth.expire();
+ LOK_ASSERT(auth.isExpired());
+ }
+
+ // resetAccessToken should clear expired state.
+ {
+ Authorization auth(Authorization::Type::Token, "tok5", false);
+ auth.expire();
+ LOK_ASSERT(auth.isExpired());
+ const auto futureMs = std::chrono::system_clock::now().time_since_epoch() +
+ std::chrono::hours(1);
+ auth.resetAccessToken("tok5_new",
+ std::chrono::duration_cast<duration>(futureMs));
+ LOK_ASSERT(!auth.isExpired());
+ LOK_ASSERT(!auth.needTokenRefresh());
+ }
+
+ // resetAccessToken with a past expiry should be expired.
+ {
+ Authorization auth(Authorization::Type::Token, "tok6", false);
+ const auto pastMs = std::chrono::system_clock::now().time_since_epoch() -
+ std::chrono::seconds(1);
+ auth.resetAccessToken("tok6_new",
+ std::chrono::duration_cast<duration>(pastMs));
+ LOK_ASSERT(auth.isExpired());
+ }
+
+ // Authorization::create with access_token_ttl should set expiry.
+ {
+ const auto futureMs = std::chrono::duration_cast<duration>(
+ std::chrono::system_clock::now().time_since_epoch() + std::chrono::hours(1));
+ const std::string uri = "http://host/wopi/files/0?access_token=secret&access_token_ttl=" +
+ std::to_string(futureMs.count());
+ Authorization auth = Authorization::create(uri);
+ LOK_ASSERT(!auth.isExpired());
+ }
+
+ // Authorization::create with past access_token_ttl should be expired.
+ {
+ const auto pastMs = std::chrono::duration_cast<duration>(
+ std::chrono::system_clock::now().time_since_epoch() - std::chrono::seconds(1));
+ const std::string uri = "http://host/wopi/files/0?access_token=secret&access_token_ttl=" +
+ std::to_string(pastMs.count());
+ Authorization auth = Authorization::create(uri);
+ LOK_ASSERT(auth.isExpired());
+ }
+
+ // Token refresh: startTokenRefresh, isRefreshingToken, isTokenRefreshTimedOut.
+ {
+ Authorization auth(Authorization::Type::Token, "tok7", false);
+ LOK_ASSERT(!auth.isRefreshingToken());
+
+ auth.startTokenRefresh(std::chrono::seconds(1));
+ LOK_ASSERT(auth.isRefreshingToken());
+ LOK_ASSERT(auth.needTokenRefresh());
+
+ // Should not have timed out yet.
+ LOK_ASSERT(!auth.isTokenRefreshTimedOut());
+
+ // Reset should clear the refreshing state.
+ const auto futureMs = std::chrono::system_clock::now().time_since_epoch() +
+ std::chrono::hours(1);
+ auth.resetAccessToken("tok7_new",
+ std::chrono::duration_cast<duration>(futureMs));
+ LOK_ASSERT(!auth.isRefreshingToken());
+ LOK_ASSERT(!auth.needTokenRefresh());
+ }
+
+ // Type::None should not be expired.
+ {
+ Authorization auth(Authorization::Type::None, "", false);
+ LOK_ASSERT(!auth.isExpired());
+ LOK_ASSERT(!auth.needTokenRefresh());
+ }
+}
+
void RequestDetailsTests::testSanitizePercent()
{
constexpr std::string_view testname = __func__;
commit 52fee95bb419a38402bd6bf770af16f30b000069
Author: Ashod Nakashian <ashod.n...@collabora.co.uk>
AuthorDate: Mon Sep 22 18:55:01 2025 -0400
Commit: Ashod Nakashian <ashod.n...@collabora.co.uk>
CommitDate: Wed Apr 29 09:23:30 2026 +0000

wsd: resetaccesstoken supports access_token_ttl

Adds an optional expiry time to the resetaccesstoken
command. Authorization object also adds support
to tracking this expiry time.

Change-Id: I2c3b810bf73d6e03fc1ef22fd8fb6c08ee797efb
Signed-off-by: Ashod Nakashian <ashod.n...@collabora.co.uk>
Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/1707
Reviewed-by: Caolán McNamara <caolan....@collabora.com>
Tested-by: Jenkins CPCI <rel...@collaboraoffice.com>
Tested-by: Caolán McNamara <caolan....@collabora.com>

diff --git a/browser/html/framed.doc.html b/browser/html/framed.doc.html
index 57239e6c86a2..7d75761cc46f 100644
--- a/browser/html/framed.doc.html
+++ b/browser/html/framed.doc.html
@@ -293,9 +293,9 @@
post({'MessageId': messageId, 'Values': values});
}

- function reset_access_token(accesstoken) {
+ function reset_access_token(accesstoken, ttl) {
post({'MessageId': 'Reset_Access_Token',
- 'Values': { 'token': accesstoken, }
+ 'Values': { 'token': accesstoken, 'ttl': ttl }
});
}

@@ -597,9 +597,14 @@

<form class="vbox framed">
<h3>New Access-Token</h3>
+ <label for="new-access-token"><b>Access Token:</b></label>
<textarea name="new-access-token" id="new-access-token" value="" rows="1" cols="30">123456789AA</textarea>
+ <label for="new-access-token-ttl"><b>Access Token TTL (millis since epoch when the token will expire):</b></label>
+ <textarea name="new-access-token-ttl" id="new-access-token-ttl" value="" rows="1" cols="30"></textarea>
<div>
- <button onclick="reset_access_token(document.getElementById('new-access-token').value); return false;">Reset Access-Token</button>
+ <button
+ onclick="reset_access_token(document.getElementById('new-access-token').value, document.getElementById('new-access-token-ttl').value); return false;">Reset
+ Access-Token</button>
</div>

<h3>User State</h3>
diff --git a/browser/src/map/handler/Map.WOPI.js b/browser/src/map/handler/Map.WOPI.js
index c44dbd81ee11..12f007040449 100644
--- a/browser/src/map/handler/Map.WOPI.js
+++ b/browser/src/map/handler/Map.WOPI.js
@@ -596,7 +596,8 @@ window.L.Map.WOPI = window.L.Handler.extend({
this._postViewsMessage('Get_Views_Resp');
}
else if (msg.MessageId === 'Reset_Access_Token') {
- app.socket.sendMessage('resetaccesstoken ' + msg.Values.token);
+ var ttl = msg.Values && msg.Values.ttl ? msg.Values['ttl'] : '0';
+ app.socket.sendMessage('resetaccesstoken ' + msg.Values.token + ' ' + ttl);
}
else if (msg.MessageId === 'Action_Save') {
var dontTerminateEdit = msg.Values && msg.Values['DontTerminateEdit'];
diff --git a/common/Authorization.cpp b/common/Authorization.cpp
index 9e7600393549..3dbcdc3b287d 100644
--- a/common/Authorization.cpp
+++ b/common/Authorization.cpp
@@ -17,7 +17,9 @@
#include <config.h>

#include "Authorization.hpp"
+
#include <common/Log.hpp>
+#include <common/NumUtil.hpp>
#include <common/StringVector.hpp>
#include <common/Uri.hpp>

@@ -101,6 +103,7 @@ void Authorization::authorizeRequest(Poco::Net::HTTPRequest& request) const
Authorization Authorization::create(const Poco::URI& uri)
{
bool noHeader = false;
+ duration expiryEpoch = duration::zero();
Authorization::Type type = Authorization::Type::None;
std::string decoded;
for (const auto& param : uri.getQueryParameters())
@@ -119,10 +122,18 @@ Authorization Authorization::create(const Poco::URI& uri)
noHeader = true;
}
}
+ else if (param.first == "access_token_ttl")
+ {
+ expiryEpoch = duration(NumUtil::u64FromString(param.second, 0));
+ }
}

if (!decoded.empty())
- return Authorization(type, std::move(decoded), noHeader);
+ {
+ Authorization auth(type, std::move(decoded), noHeader);
+ auth.setExpiryEpoch(expiryEpoch);
+ return auth;
+ }

return Authorization();
}
diff --git a/common/Authorization.hpp b/common/Authorization.hpp
index 5697c90d33f1..17ddd170b043 100644
--- a/common/Authorization.hpp
+++ b/common/Authorization.hpp
@@ -15,6 +15,7 @@

#include <common/StateEnum.hpp>

+#include <chrono>
#include <string>

namespace Poco
@@ -31,6 +32,8 @@ class URI;
/// Class to keep the authorization data, which can be either access_token or access_header.
class Authorization
{
+ using duration = std::chrono::milliseconds;
+
public:
STATE_ENUM(Type,
None, ///< Unlike Expired, this implies no Authorization needed.
@@ -42,11 +45,11 @@ public:
private:
std::string _data;
Type _type;
+ duration _expiryEpoch; ///< Milliseconds from the epoch when the access_token will expire.
bool _noHeader;

Authorization()
- : _type(Type::None)
- , _noHeader(false)
+ : Authorization(Type::None, std::string(), false)
{
}

@@ -54,6 +57,7 @@ public:
Authorization(Type type, std::string data, bool noHeader)
: _data(std::move(data))
, _type(type)
+ , _expiryEpoch(duration::zero())
, _noHeader(noHeader)
{
}
@@ -63,17 +67,25 @@ public:
static Authorization create(const Poco::URI& uri);
static Authorization create(const std::string& uri);

- void resetAccessToken(std::string accessToken)
+ void resetAccessToken(std::string accessToken, duration expiryEpoch)
{
_type = Type::Token;
_data = std::move(accessToken);
+ _expiryEpoch = expiryEpoch;
}

/// Expire the Authorization data.
void expire() { _type = Type::Expired; }

+ void setExpiryEpoch(duration epochMs) { _expiryEpoch = epochMs; }
+
/// Returns true iff the Authorization data is invalid.
- bool isExpired() const { return _type == Type::Expired; }
+ bool isExpired() const
+ {
+ return _type == Type::Expired ||
+ (_expiryEpoch > duration::zero() &&
+ std::chrono::system_clock::now().time_since_epoch() > _expiryEpoch);
+ }

/// Set the access_token parameter to the given URI.
void authorizeURI(Poco::URI& uri) const;
diff --git a/kit/ChildSession.cpp b/kit/ChildSession.cpp
index bcfde62fa6de..3e3f58b0a9bd 100644
--- a/kit/ChildSession.cpp
+++ b/kit/ChildSession.cpp
@@ -19,7 +19,6 @@
#include "ChildSession.hpp"

#include <common/Anonymizer.hpp>
-#include <common/Authorization.hpp>
#include <common/Clipboard.hpp>
#include <common/CommandControl.hpp>
#include <common/ConfigUtil.hpp>
diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index f480f94987dc..ef0876791ffb 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -51,6 +51,8 @@
#include <Poco/URI.h>

#include <cctype>
+#include <chrono>
+#include <cstdint>
#include <ios>
#include <map>
#include <memory>
@@ -1349,14 +1351,20 @@ bool ClientSession::_handleInput(const char *buffer, int length)
#endif
else if (tokens.equals(0, "resetaccesstoken"))
{
- if (tokens.size() != 2)
+ if (tokens.size() <= 1 || tokens.size() > 3)
{
LOG_ERR("Bad syntax for: " << tokens[0]);
- sendTextFrameAndLogError("error: cmd=resetaccesstoken kind=syntax");
+ sendTextFrameAndLogError("error: cmd=" + tokens[0] + " kind=syntax");
return false;
}

- _auth.resetAccessToken(tokens[1]);
+ // Get the access_token_ttl, if provided. 0 means unknown/missing.
+ const auto expiryEpoch = std::chrono::milliseconds(
+ (tokens.size() == 3) ? NumUtil::u64FromString(tokens[2], 0) : 0);
+
+ LOG_DBG("Resetting access token for " << getName() << " with expiry at " << expiryEpoch
+ << ": " << tokens[1]);
+ _auth.resetAccessToken(tokens[1], expiryEpoch);
return true;
}
#if !MOBILEAPP && !WASMAPP
diff --git a/wsd/protocol.txt b/wsd/protocol.txt
index e9287581756d..378234e0ffd4 100644
--- a/wsd/protocol.txt
+++ b/wsd/protocol.txt
@@ -470,12 +470,13 @@ getslide hash=<slideHash> part=<slidePart> width=<canvasWidth> height=<canvasHei

<devicePixelRatio>: The device pixel ratio, typically window.devicePixelRatio, which adjusts the rendering quality for HiDPI displays.

-resetaccesstoken <token=<accessToken>>
+resetaccesstoken <token=<accessToken>> [<expiryEpoch>]

This command ensures that the client uses the updated access token for further requests.
Resets the access token for authentication or session management.

- <token>: The new access token that replaces the previous one.
+ - <expiryEpoch>: The time from Unix/Javascript epoch when the access token will expire. Optional. 0 means unknown/missing.

renamefile <filename=<newFilename>>

commit 546777e745dc8f92997cfaafb4b2bb96bc6d9e37
Author: Ashod Nakashian <ashod.n...@collabora.co.uk>
AuthorDate: Thu Apr 23 05:31:55 2026 +0000
Commit: Ashod Nakashian <ashod.n...@collabora.co.uk>
CommitDate: Wed Apr 29 09:23:07 2026 +0000

browser: use legacy WS URL path for non-WOPI documents

Change-Id: I49d08ef5ab4594132765b3ade2e9f1cf1e13fbf9
Signed-off-by: Ashod Nakashian <ashod.n...@collabora.co.uk>
Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/1706
Tested-by: Caolán McNamara <caolan....@collabora.com>
Tested-by: Jenkins CPCI <rel...@collaboraoffice.com>
Reviewed-by: Caolán McNamara <caolan....@collabora.com>

diff --git a/browser/js/global.js b/browser/js/global.js
index fe1045a85887..7ba67074f65b 100644
--- a/browser/js/global.js
+++ b/browser/js/global.js
@@ -2042,7 +2042,7 @@ function showWelcomeSVG() {
global.socket = new global.FakeWebSocket();
global.TheFakeWebSocket = global.socket;
} else {
- if (global.enableExperimentalFeatures) {
+ if (global.enableExperimentalFeatures && global.wopiSrc) {
var websocketURI = global.makeWopiCoolWsUrl(global.makeWsUrl('/cool'), docParams);
} else {
// The URL may already contain a query (e.g., 'http://server.tld/foo/wopi/files/bar?desktop=baz') - then just append more params
diff --git a/browser/src/app/Socket.ts b/browser/src/app/Socket.ts
index 2ab782e77f4b..77d0ea283caa 100644
--- a/browser/src/app/Socket.ts
+++ b/browser/src/app/Socket.ts
@@ -236,8 +236,8 @@ class Socket {
}

private getWebSocketBaseURI(map: MapInterface): string {
- if (window.enableExperimentalFeatures) {
- // Use the new Cool WS URL.
+ if (window.enableExperimentalFeatures && map.options.wopiSrc) {
+ // Use the new Cool WS URL for WOPI documents.
return window.makeWopiCoolWsUrl(
window.makeWsUrl('/cool'),
$.param(map.options.docParams),

Reply all
Reply to author
Forward
0 new messages