Skip to content

Commit b1fa00c

Browse files
committed
[Server] Add XrdSecoauth2 bearer JWT authentication and xrdtoken utility.
Add sec.protocol oauth2 for TLS-only OAuth2/OIDC bearer authentication, shared JWT validation in libXrdUtils, HTTPS bearer integration, client token pickup, and the xrdtoken helper for obtaining tokens from common providers. Plugin (libXrdSecoauth2, src/XrdSecoauth2/): - Validate RS256 JWTs with multi-issuer JWKS discovery, audience and expiry policy, validated-token cache, and optional on-disk JWKS cache - Map identity from configurable claims; populate XrdSecEntity role, grps, and extension attributes from JWT claims and issuer path options - Client getCredentials() reads BEARER_TOKEN, BEARER_TOKEN_FILE, XDG_RUNTIME_DIR/bt_u<uid>, and /tmp/bt_u<uid> - Client debug logging via XrdOucUtils::DebugEnabled when XrdSecDEBUG is set to 1, yes, true, or enabled Shared library (src/XrdOAuth2/, built into libXrdUtils): - XrdOAuth2: JWT parsing and verification, INI config, issuer policies, and C++20 helpers for oauth2 credential framing and entity fields - Avoid std::format (unavailable on older libstdc++ in CI); use portable string concatenation instead - makeCredentials() allocates with malloc() to match XrdSecBuffer::free() (new[]/free mismatch caused heap corruption on Linux/glibc) - Keep trim/to-lower/bool parsing helpers local to XrdOAuth2.cc; add generic XrdOucUtils::Enabled/DebugEnabled for env-var debug logging - Use explicit std::atomic<int> for globals also declared in XrdOAuth2Detail.hh (GCC rejects std::atomic CTAD there) - Add XRootD-style method banners throughout XrdOAuth2.cc HTTP (http.oauth2): - Optional or required bearer authentication on HTTPS via the security framework; re-authenticate when the Authorization token changes - Reuse a single XrdSecService with xrootd via shared process environment Initialization: - InitSecProtocol is idempotent so oauth2 globals initialize once when both xrootd and HTTP load sec.protocol oauth2 in the same process JWT validation hardening (src/XrdOAuth2/): - Rate-limit and condition forced JWKS refresh on signature failure (only for an unknown kid, with a per-issuer cooldown) to remove a DoS amplification vector; add a short-TTL negative-token cache - Single-flight config reload so a config change cannot fan out into concurrent parse and JWKS-fetch storms on the auth hot path - Sanitize the resolved identity before it becomes XrdSecEntity.name; enforce a 2048-bit minimum RSA key size; warn when an issuer has no audience configured - sha256hex fails closed (never falls back to the raw token as a cache key); harden JWKS disk cache reads/writes with O_NOFOLLOW + fstat; accept float-valued NumericDate claims; clear partial init state on failure HTTP integration hardening (src/XrdHttp/): - Bind bearer authentication to the request rather than the connection: the same token presented again is accepted without re-validation only while it has not yet expired (a pure clock comparison via the recorded token exp, exposed cheaply through XrdOAuth2::CachedTokenExpiry), so an expired or revoked token cannot be reused for the lifetime of a keep-alive connection - Require a valid bearer token on every request in require mode; clear the previous token's identity and attributes before applying a changed token so stale scopes/groups/paths cannot leak (adds XrdSecEntityAttr::Reset) - Return RFC 6750 WWW-Authenticate challenges on 4xx, reject ambiguous or duplicate Authorization credentials with 400, and document preferring the Authorization header over the authz query parameter - Modernize the OAuth2 HTTP path with enum class OAuth2HttpMode, std::string_view, chrono-based expiry checks, RAII SecProtocolHolder, and shared XrdOAuth2 helpers instead of local C-style strip/fingerprint/ credential code Tooling and tests: - utils/xrdtoken: device and PKCE flows for CERN, CERNOIDC, IAM/WLCG, Google, and GitHub, with OIDC nonce validation on id_tokens - 57 unit tests (tests/XrdSec/XrdSecOAuth2.cc), example WLCG IAM config, and README - OAuth2 unit tests link libXrdUtils once and use XrdOAuth2Detail.hh for detail:: access (avoids compiling XrdOAuth2.cc twice, which caused free(): invalid pointer at process exit on Linux/glibc) Install libXrdSecoauth2 in Debian and RPM packaging. Assisted-by: Cursor:Opus-4.8 CursorAI Assisted-by: Cursor:Composer-2.5 CursorAI
1 parent 4ec63a2 commit b1fa00c

27 files changed

Lines changed: 7111 additions & 7 deletions

debian/xrootd-plugins.install

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
/usr/lib/*/libXrdSecsss-6.so
1111
/usr/lib/*/libXrdSecunix-6.so
1212
/usr/lib/*/libXrdSecztn-6.so
13+
/usr/lib/*/libXrdSecoauth2-6.so

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ add_subdirectory( XrdNet )
5353
add_subdirectory( XrdSut )
5454
add_subdirectory( XrdSys )
5555
add_subdirectory( XrdOuc )
56+
add_subdirectory( XrdOAuth2 )
5657
add_subdirectory( XrdTls )
5758
add_subdirectory( XrdRmc )
5859

@@ -63,6 +64,7 @@ add_subdirectory( XrdPosix )
6364
add_subdirectory( XrdSec )
6465
add_subdirectory( XrdSecgsi )
6566
add_subdirectory( XrdSeckrb5 )
67+
add_subdirectory( XrdSecoauth2 )
6668
add_subdirectory( XrdSecpwd )
6769
add_subdirectory( XrdSecsss )
6870
add_subdirectory( XrdSecunix )

src/XrdHeaders.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ set( XROOTD_PUBLIC_HEADERS
6767
XrdOuc/XrdOuca2x.hh
6868
XrdOuc/XrdOucEnum.hh
6969
XrdOuc/XrdOucCompiler.hh
70+
XrdOAuth2/XrdOAuth2.hh
7071
XrdPosix/XrdPosix.hh
7172
XrdPosix/XrdPosixCache.hh
7273
XrdPosix/XrdPosixCallBack.hh

src/XrdHttp/XrdHttpProtocol.cc

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
#include "XrdTls/XrdTlsContext.hh"
4747
#include "XrdOuc/XrdOucUtils.hh"
4848
#include "XrdOuc/XrdOucPrivateUtils.hh"
49+
#include "XrdSec/XrdSecLoadSecurity.hh"
4950
#include "XrdHttpCors/XrdHttpCors.hh"
5051

5152
#include <charconv>
@@ -119,6 +120,7 @@ XrdScheduler *XrdHttpProtocol::Sched = 0; // System scheduler
119120
XrdBuffManager *XrdHttpProtocol::BPool = 0; // Buffer manager
120121
XrdSysError XrdHttpProtocol::eDest = 0; // Error message handler
121122
XrdSecService *XrdHttpProtocol::CIA = 0; // Authentication Server
123+
XrdOucEnv *XrdHttpProtocol::configEnv = 0;
122124
int XrdHttpProtocol::m_bio_type = 0; // BIO type identifier for our custom BIO.
123125
BIO_METHOD *XrdHttpProtocol::m_bio_method = NULL; // BIO method constructor.
124126
char *XrdHttpProtocol::xrd_cslist = nullptr;
@@ -155,6 +157,10 @@ bool xrdctxVer = false;
155157

156158
using namespace XrdHttpProtoInfo;
157159

160+
XrdHttpProtocol::OAuth2HttpMode XrdHttpProtocol::oauth2HttpMode =
161+
XrdHttpProtocol::OAuth2HttpMode::Off;
162+
std::string_view XrdHttpProtocol::oauth2ConfigFN;
163+
158164
/******************************************************************************/
159165
/* P r o t o c o l M a n a g e m e n t S t a c k s */
160166
/******************************************************************************/
@@ -806,6 +812,13 @@ int XrdHttpProtocol::Process(XrdLink *lp) // We ignore the argument here
806812

807813

808814

815+
// Bearer OAuth2 authentication over HTTPS (via sec.protocol oauth2 / CIA).
816+
if (ishttps && ssldone
817+
&& oauth2HttpMode != OAuth2HttpMode::Off
818+
&& HandleOAuth2Authentication()) {
819+
return -1;
820+
}
821+
809822
// Now we have everything that is needed to try the login
810823
// Remember that if there is an exthandler then it has the responsibility
811824
// for authorization in the paths that it manages
@@ -904,6 +917,8 @@ int XrdHttpProtocol::Stats(char *buff, int blen, int do_sync) {
904917
eDest.Say("Config http." x " overrides the xrd." y " directive.")
905918

906919
int XrdHttpProtocol::Config(const char *ConfigFN, XrdOucEnv *myEnv) {
920+
XrdHttpProtocol::oauth2ConfigFN = ConfigFN;
921+
XrdHttpProtocol::configEnv = myEnv;
907922
XrdOucEnv cfgEnv;
908923
XrdOucStream Config(&eDest, getenv("XRDINSTANCE"), &cfgEnv, "=====> ");
909924
std::vector<extHInfo> extHIVec;
@@ -1005,6 +1020,7 @@ int XrdHttpProtocol::Config(const char *ConfigFN, XrdOucEnv *myEnv) {
10051020
else if TS_Xeq("tlsreuse", xtlsreuse);
10061021
else if TS_Xeq("auth", xauth);
10071022
else if TS_Xeq("tlsclientauth", xtlsclientauth);
1023+
else if TS_Xeq("oauth2", xoauth2);
10081024
else if TS_Xeq("maxdelay", xmaxdelay);
10091025
else {
10101026
eDest.Say("Config warning: ignoring unknown directive '", var, "'.");
@@ -1990,6 +2006,8 @@ void XrdHttpProtocol::Reset() {
19902006
ishttps = false;
19912007
ssldone = false;
19922008

2009+
oauth2BearerTokKey.clear();
2010+
oauth2BearerTokExp = 0;
19932011
Bridge = 0;
19942012
ssl = 0;
19952013
sbio = 0;
@@ -3001,6 +3019,29 @@ int XrdHttpProtocol::xtlsclientauth(XrdOucStream &Config) {
30013019
return 1;
30023020
}
30033021

3022+
int XrdHttpProtocol::xoauth2(XrdOucStream &Config) {
3023+
char *val = Config.GetWord();
3024+
if (!val || !val[0])
3025+
{eDest.Emsg("Config", "http.oauth2 argument not specified"); return 1;}
3026+
3027+
if (!strcmp(val, "on") || !strcmp(val, "optional"))
3028+
{oauth2HttpMode = OAuth2HttpMode::Optional;
3029+
return 0;
3030+
}
3031+
if (!strcmp(val, "require"))
3032+
{oauth2HttpMode = OAuth2HttpMode::Require;
3033+
return 0;
3034+
}
3035+
if (val[0] == '-')
3036+
{eDest.Emsg("Config", "http.oauth2 inline parameters are not supported;",
3037+
"configure OAuth2 via sec.protparm oauth2 and sec.protocol oauth2");
3038+
return 1;
3039+
}
3040+
3041+
eDest.Emsg("Config", "invalid http.oauth2 parameter -", val);
3042+
return 1;
3043+
}
3044+
30043045
int XrdHttpProtocol::xauth(XrdOucStream &Config) {
30053046
char *val = Config.GetWord();
30063047
if(val) {

src/XrdHttp/XrdHttpProtocol.hh

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949

5050
#include <chrono>
5151
#include <cstdlib>
52+
#include <string_view>
5253
#include <openssl/ssl.h>
5354
#include <sys/types.h>
5455
#include <unistd.h>
@@ -170,6 +171,10 @@ private:
170171
/// @return 0 if successful, otherwise error
171172
int HandleAuthentication(XrdLink* lp);
172173

174+
/// Validate a Bearer OAuth2 token and populate SecEntity.
175+
/// @return 0 if successful or not applicable, otherwise error
176+
int HandleOAuth2Authentication();
177+
173178
/// After the SSL handshake, retrieve the VOMS info and the various stuff
174179
/// that is needed for autorization
175180
int GetVOMSData(XrdLink *lp);
@@ -229,8 +234,17 @@ private:
229234
static int xtlsreuse(XrdOucStream &Config);
230235
static int xauth(XrdOucStream &Config);
231236
static int xtlsclientauth(XrdOucStream &Config);
237+
static int xoauth2(XrdOucStream &Config);
232238
static int xmaxdelay(XrdOucStream &Config);
233239

240+
/// HTTPS bearer OAuth2 policy for http.oauth2.
241+
enum class OAuth2HttpMode { Off = 0, Optional = 1, Require = 2 };
242+
243+
static OAuth2HttpMode oauth2HttpMode;
244+
245+
/// Config file path used to load the security framework for http.oauth2.
246+
static std::string_view oauth2ConfigFN;
247+
234248
static bool isRequiredXtractor; // If true treat secxtractor errors as fatal
235249
static XrdHttpSecXtractor *secxtractor;
236250

@@ -363,6 +377,9 @@ protected:
363377
static XrdSysError eDest; // Error message handler
364378
static XrdSecService *CIA; // Authentication Server
365379

380+
/// Shared process environment passed to Config(); used to reuse security.
381+
static XrdOucEnv *configEnv;
382+
366383
/// The link we are bound to
367384
XrdLink *Link;
368385

@@ -376,7 +393,15 @@ protected:
376393
/// The Bridge that we use to exercise the xrootd internals
377394
XrdXrootd::Bridge *Bridge;
378395

379-
396+
/// SHA-256 fingerprint of the validated OAuth2 bearer token on this connection.
397+
std::string oauth2BearerTokKey;
398+
399+
/// Expiry (unix seconds) of the validated OAuth2 bearer token above. While the
400+
/// same token is presented and this time has not passed, validation is skipped
401+
/// (a pure clock comparison); once it passes the token is re-validated so an
402+
/// expired credential cannot be reused for the lifetime of the connection.
403+
uint64_t oauth2BearerTokExp = 0;
404+
380405
/// Area for coordinating request and responses to/from the bridge
381406
/// This also can process HTTP/DAV stuff
382407
XrdHttpReq CurrentReq;

0 commit comments

Comments
 (0)