Skip to content

Commit cc3e9ed

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> 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) 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 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 cc3e9ed

27 files changed

Lines changed: 6737 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: 38 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,9 @@ bool xrdctxVer = false;
155157

156158
using namespace XrdHttpProtoInfo;
157159

160+
int XrdHttpProtocol::oauth2HttpMode = 0;
161+
const char *XrdHttpProtocol::oauth2ConfigFN = nullptr;
162+
158163
/******************************************************************************/
159164
/* P r o t o c o l M a n a g e m e n t S t a c k s */
160165
/******************************************************************************/
@@ -806,6 +811,11 @@ int XrdHttpProtocol::Process(XrdLink *lp) // We ignore the argument here
806811

807812

808813

814+
// Bearer OAuth2 authentication over HTTPS (via sec.protocol oauth2 / CIA).
815+
if (ishttps && ssldone && oauth2HttpMode && HandleOAuth2Authentication()) {
816+
return -1;
817+
}
818+
809819
// Now we have everything that is needed to try the login
810820
// Remember that if there is an exthandler then it has the responsibility
811821
// for authorization in the paths that it manages
@@ -904,6 +914,8 @@ int XrdHttpProtocol::Stats(char *buff, int blen, int do_sync) {
904914
eDest.Say("Config http." x " overrides the xrd." y " directive.")
905915

906916
int XrdHttpProtocol::Config(const char *ConfigFN, XrdOucEnv *myEnv) {
917+
XrdHttpProtocol::oauth2ConfigFN = ConfigFN;
918+
XrdHttpProtocol::configEnv = myEnv;
907919
XrdOucEnv cfgEnv;
908920
XrdOucStream Config(&eDest, getenv("XRDINSTANCE"), &cfgEnv, "=====> ");
909921
std::vector<extHInfo> extHIVec;
@@ -1005,6 +1017,7 @@ int XrdHttpProtocol::Config(const char *ConfigFN, XrdOucEnv *myEnv) {
10051017
else if TS_Xeq("tlsreuse", xtlsreuse);
10061018
else if TS_Xeq("auth", xauth);
10071019
else if TS_Xeq("tlsclientauth", xtlsclientauth);
1020+
else if TS_Xeq("oauth2", xoauth2);
10081021
else if TS_Xeq("maxdelay", xmaxdelay);
10091022
else {
10101023
eDest.Say("Config warning: ignoring unknown directive '", var, "'.");
@@ -1990,6 +2003,8 @@ void XrdHttpProtocol::Reset() {
19902003
ishttps = false;
19912004
ssldone = false;
19922005

2006+
oauth2BearerTokKey.clear();
2007+
oauth2BearerTokExp = 0;
19932008
Bridge = 0;
19942009
ssl = 0;
19952010
sbio = 0;
@@ -3001,6 +3016,29 @@ int XrdHttpProtocol::xtlsclientauth(XrdOucStream &Config) {
30013016
return 1;
30023017
}
30033018

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

src/XrdHttp/XrdHttpProtocol.hh

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,10 @@ private:
170170
/// @return 0 if successful, otherwise error
171171
int HandleAuthentication(XrdLink* lp);
172172

173+
/// Validate a Bearer OAuth2 token and populate SecEntity.
174+
/// @return 0 if successful or not applicable, otherwise error
175+
int HandleOAuth2Authentication();
176+
173177
/// After the SSL handshake, retrieve the VOMS info and the various stuff
174178
/// that is needed for autorization
175179
int GetVOMSData(XrdLink *lp);
@@ -229,8 +233,15 @@ private:
229233
static int xtlsreuse(XrdOucStream &Config);
230234
static int xauth(XrdOucStream &Config);
231235
static int xtlsclientauth(XrdOucStream &Config);
236+
static int xoauth2(XrdOucStream &Config);
232237
static int xmaxdelay(XrdOucStream &Config);
233238

239+
/// 0=disabled, 1=optional bearer OAuth2, 2=require bearer OAuth2
240+
static int oauth2HttpMode;
241+
242+
/// Config file path used to load the security framework for http.oauth2.
243+
static const char *oauth2ConfigFN;
244+
234245
static bool isRequiredXtractor; // If true treat secxtractor errors as fatal
235246
static XrdHttpSecXtractor *secxtractor;
236247

@@ -363,6 +374,9 @@ protected:
363374
static XrdSysError eDest; // Error message handler
364375
static XrdSecService *CIA; // Authentication Server
365376

377+
/// Shared process environment passed to Config(); used to reuse security.
378+
static XrdOucEnv *configEnv;
379+
366380
/// The link we are bound to
367381
XrdLink *Link;
368382

@@ -376,7 +390,15 @@ protected:
376390
/// The Bridge that we use to exercise the xrootd internals
377391
XrdXrootd::Bridge *Bridge;
378392

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

0 commit comments

Comments
 (0)