Skip to content

Commit 52ebd4d

Browse files
committed
[Server] add XrdSecoidc OIDC plugin, shared validation, and HTTPS bearer auth.
Introduce sec.protocol oidc for mandatory-TLS JWT authentication: RS256 signature checks against per-issuer JWKS, issuer and audience policy, expiry handling, ordered or forced identity claims, email-map username mapping, INI configuration with issuer/email-map hot reload, and a bounded validated-token cache. Extract shared logic into libXrdOucOIDC for XrdHttp http.oidc bearer authentication on HTTPS, including configurable entity-claim mapping into XrdSecEntity attributes, per-issuer base_path and repeatable restricted_path export, and keep-alive detection when the Authorization token changes (re-validate JWT and re-login the xrootd Bridge). Move common string parsing helpers to XrdOucUtils, build XrdSecoidc in client-only trees, and replace the xrdsso shell helpers with a Python xrdtoken CLI for CERN, CERNOIDC, Google, and GitHub token acquisition. Adds unit tests, CMake/packaging wiring, and README documentation. Assisted-by: Cursor:Opus-4.8 CursorAI Assisted-by: Cursor:Composer-2.5 CursorAI [Server] add WLCG IAM support: scope export, xrdtoken, and docs. Normalize JSON-array scope claims for XrdAccToken, add IAM/WLCG xrdtoken providers, and document CERN SSO vs WLCG IAM configuration. Assisted-by: Cursor:Opus-4.8 CursorAI
1 parent ace1d2a commit 52ebd4d

22 files changed

Lines changed: 5642 additions & 3 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/*/libXrdSecoidc-6.so

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ add_subdirectory( XrdPosix )
6262
add_subdirectory( XrdSec )
6363
add_subdirectory( XrdSecgsi )
6464
add_subdirectory( XrdSeckrb5 )
65+
add_subdirectory( XrdSecoidc )
6566
add_subdirectory( XrdSecpwd )
6667
add_subdirectory( XrdSecsss )
6768
add_subdirectory( XrdSecunix )

src/XrdHttp/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@ set_target_properties(XrdHttpUtils
2828
VERSION ${XRootD_LIBVERSION}
2929
)
3030

31+
set(XrdOucOIDC XrdOucOIDC-${PLUGIN_VERSION})
32+
3133
target_link_libraries(XrdHttpUtils
3234
PRIVATE
3335
XrdServer
3436
XrdUtils
3537
XrdCrypto
38+
${XrdOucOIDC}
3639
PUBLIC
3740
OpenSSL::SSL
3841
OpenSSL::Crypto

src/XrdHttp/XrdHttpProtocol.cc

Lines changed: 55 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 "XrdOuc/XrdOucOIDC.hh"
4950
#include "XrdHttpCors/XrdHttpCors.hh"
5051

5152
#include <charconv>
@@ -155,6 +156,8 @@ bool xrdctxVer = false;
155156

156157
using namespace XrdHttpProtoInfo;
157158

159+
int XrdHttpProtocol::oidcHttpMode = 0;
160+
158161
/******************************************************************************/
159162
/* P r o t o c o l M a n a g e m e n t S t a c k s */
160163
/******************************************************************************/
@@ -806,6 +809,11 @@ int XrdHttpProtocol::Process(XrdLink *lp) // We ignore the argument here
806809

807810

808811

812+
// Bearer OIDC authentication over HTTPS (shared with sec.protocol oidc).
813+
if (ishttps && ssldone && oidcHttpMode && HandleOidcAuthentication()) {
814+
return -1;
815+
}
816+
809817
// Now we have everything that is needed to try the login
810818
// Remember that if there is an exthandler then it has the responsibility
811819
// for authorization in the paths that it manages
@@ -1005,6 +1013,7 @@ int XrdHttpProtocol::Config(const char *ConfigFN, XrdOucEnv *myEnv) {
10051013
else if TS_Xeq("tlsreuse", xtlsreuse);
10061014
else if TS_Xeq("auth", xauth);
10071015
else if TS_Xeq("tlsclientauth", xtlsclientauth);
1016+
else if TS_Xeq("oidc", xoidc);
10081017
else if TS_Xeq("maxdelay", xmaxdelay);
10091018
else {
10101019
eDest.Say("Config warning: ignoring unknown directive '", var, "'.");
@@ -1990,6 +1999,7 @@ void XrdHttpProtocol::Reset() {
19901999
ishttps = false;
19912000
ssldone = false;
19922001

2002+
oidcBearerTokKey.clear();
19932003
Bridge = 0;
19942004
ssl = 0;
19952005
sbio = 0;
@@ -3001,6 +3011,51 @@ int XrdHttpProtocol::xtlsclientauth(XrdOucStream &Config) {
30013011
return 1;
30023012
}
30033013

3014+
int XrdHttpProtocol::xoidc(XrdOucStream &Config) {
3015+
char *val = Config.GetWord();
3016+
if (!val || !val[0])
3017+
{eDest.Emsg("Config", "http.oidc argument not specified"); return 1;}
3018+
3019+
std::string parms;
3020+
bool needInit = false;
3021+
3022+
if (!strcmp(val, "on") || !strcmp(val, "optional"))
3023+
{oidcHttpMode = 1;
3024+
needInit = !XrdOucOIDC::IsConfigured();
3025+
}
3026+
else if (!strcmp(val, "require"))
3027+
{oidcHttpMode = 2;
3028+
needInit = !XrdOucOIDC::IsConfigured();
3029+
}
3030+
else if (val[0] == '-')
3031+
{oidcHttpMode = 1;
3032+
parms = val;
3033+
char *w = nullptr;
3034+
while ((w = Config.GetWord()))
3035+
{parms.push_back(' ');
3036+
parms += w;
3037+
}
3038+
needInit = true;
3039+
}
3040+
else
3041+
{eDest.Emsg("Config", "invalid http.oidc parameter -", val); return 1;}
3042+
3043+
if (needInit)
3044+
{std::string emsg;
3045+
const char *initParms = parms.empty() ? nullptr : parms.c_str();
3046+
if (!XrdOucOIDC::Init(eDest.logger(), initParms, emsg))
3047+
{eDest.Emsg("Config", "http.oidc initialization failed:", emsg.c_str());
3048+
return 1;
3049+
}
3050+
}
3051+
else if (!XrdOucOIDC::IsConfigured())
3052+
{eDest.Emsg("Config", "http.oidc requires OIDC configuration via sec.protocol oidc or inline parameters");
3053+
return 1;
3054+
}
3055+
3056+
return 0;
3057+
}
3058+
30043059
int XrdHttpProtocol::xauth(XrdOucStream &Config) {
30053060
char *val = Config.GetWord();
30063061
if(val) {

src/XrdHttp/XrdHttpProtocol.hh

Lines changed: 11 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 OIDC token and populate SecEntity.
174+
/// @return 0 if successful or not applicable, otherwise error
175+
int HandleOidcAuthentication();
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,12 @@ private:
229233
static int xtlsreuse(XrdOucStream &Config);
230234
static int xauth(XrdOucStream &Config);
231235
static int xtlsclientauth(XrdOucStream &Config);
236+
static int xoidc(XrdOucStream &Config);
232237
static int xmaxdelay(XrdOucStream &Config);
233238

239+
/// 0=disabled, 1=optional bearer OIDC, 2=require bearer OIDC
240+
static int oidcHttpMode;
241+
234242
static bool isRequiredXtractor; // If true treat secxtractor errors as fatal
235243
static XrdHttpSecXtractor *secxtractor;
236244

@@ -376,7 +384,9 @@ protected:
376384
/// The Bridge that we use to exercise the xrootd internals
377385
XrdXrootd::Bridge *Bridge;
378386

379-
387+
/// SHA-256 fingerprint of the validated OIDC bearer token on this connection.
388+
std::string oidcBearerTokKey;
389+
380390
/// Area for coordinating request and responses to/from the bridge
381391
/// This also can process HTTP/DAV stuff
382392
XrdHttpReq CurrentReq;

src/XrdHttp/XrdHttpSecurity.cc

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,17 @@
1919
// along with XRootD. If not, see <http://www.gnu.org/licenses/>.
2020
//------------------------------------------------------------------------------
2121

22+
#include <cstdio>
23+
#include <cstring>
24+
#include <map>
25+
#include <string>
26+
27+
#include <openssl/evp.h>
28+
2229
#include "XrdHttpProtocol.hh"
2330
#include "XrdHttpTrace.hh"
2431
#include "XrdHttpSecXtractor.hh"
32+
#include "XrdOuc/XrdOucOIDC.hh"
2533
#include "Xrd/XrdLink.hh"
2634
#include "XrdCrypto/XrdCryptoX509Chain.hh"
2735
#include "XrdCrypto/XrdCryptosslAux.hh"
@@ -49,6 +57,46 @@ const char *TraceID = "Security";
4957

5058
using namespace XrdHttpProtoInfo;
5159

60+
namespace
61+
{
62+
63+
std::string bearerTokenKey(const char *tok, int tlen)
64+
{
65+
unsigned char md[EVP_MAX_MD_SIZE];
66+
unsigned int mdLen = 0;
67+
if (!tok || tlen <= 0
68+
|| !EVP_Digest(tok, static_cast<size_t>(tlen), md, &mdLen, EVP_sha256(), nullptr))
69+
return {};
70+
char hex[EVP_MAX_MD_SIZE * 2 + 1];
71+
for (unsigned int i = 0; i < mdLen; ++i)
72+
snprintf(hex + i * 2, sizeof(hex) - i * 2, "%02x", md[i]);
73+
return std::string(hex, mdLen * 2);
74+
}
75+
76+
bool extractBearerToken(const XrdHttpReq &req, std::string &token)
77+
{
78+
const std::string &cgi = req.hdr2cgistr;
79+
const char *key = "authz=";
80+
size_t pos = cgi.find(key);
81+
if (pos != std::string::npos)
82+
{size_t start = pos + strlen(key);
83+
size_t end = cgi.find('&', start);
84+
token = (end == std::string::npos ? cgi.substr(start)
85+
: cgi.substr(start, end - start));
86+
return !token.empty();
87+
}
88+
89+
for (const auto &hdr : req.allheaders)
90+
{if (!strcasecmp(hdr.first.c_str(), "authorization"))
91+
{token = hdr.second;
92+
return !token.empty();
93+
}
94+
}
95+
return false;
96+
}
97+
98+
} // namespace
99+
52100
/******************************************************************************/
53101
/* I n i t S e c u r i t y */
54102
/******************************************************************************/
@@ -86,6 +134,81 @@ bool XrdHttpProtocol::InitSecurity() {
86134
return true;
87135
}
88136

137+
/******************************************************************************/
138+
/* H a n d l e O I D C A u t h e n t i c a t i o n */
139+
/******************************************************************************/
140+
141+
int
142+
XrdHttpProtocol::HandleOidcAuthentication()
143+
{
144+
#undef TRACELINK
145+
#define TRACELINK Link
146+
147+
if (!oidcHttpMode || !XrdOucOIDC::IsConfigured()) return 0;
148+
149+
// Client-certificate identity is fixed for the TLS connection.
150+
if (SecEntity.name && strncmp(SecEntity.prot, "oidc", 4) != 0) return 0;
151+
152+
std::string bearer;
153+
if (!extractBearerToken(CurrentReq, bearer))
154+
{if (oidcHttpMode == 2 && oidcBearerTokKey.empty())
155+
{TRACEI(REQ, " OIDC bearer token required but not provided.");
156+
SendSimpleResp(401, nullptr, nullptr, "Authentication required", 0, false);
157+
return 1;
158+
}
159+
return 0;
160+
}
161+
162+
int tlen = 0;
163+
const char *tok = XrdOucOIDC::StripToken(bearer.c_str(), tlen);
164+
if (!tok || tlen <= 0)
165+
{TRACEI(REQ, " OIDC bearer token malformed.");
166+
SendSimpleResp(401, nullptr, nullptr, "Authentication failed", 0, false);
167+
return 1;
168+
}
169+
170+
const std::string tokKey = bearerTokenKey(tok, tlen);
171+
if (tokKey.empty())
172+
{TRACEI(REQ, " OIDC bearer token fingerprint failed.");
173+
SendSimpleResp(500, nullptr, nullptr, "Authentication failed", 0, false);
174+
return 1;
175+
}
176+
177+
if (!oidcBearerTokKey.empty() && tokKey == oidcBearerTokKey) return 0;
178+
179+
std::string tokStr(tok, static_cast<size_t>(tlen));
180+
std::string identity, emsg;
181+
std::map<std::string, std::string> entityAttrs;
182+
if (!XrdOucOIDC::ValidateToken(tokStr.c_str(), identity, emsg, nullptr,
183+
&entityAttrs))
184+
{TRACEI(REQ, " OIDC token validation failed: " << emsg);
185+
SendSimpleResp(401, nullptr, nullptr, "Authentication failed", 0, false);
186+
return 1;
187+
}
188+
189+
if (!oidcBearerTokKey.empty() || Bridge)
190+
{if (Bridge && !Bridge->Disc())
191+
{TRACEI(REQ, " OIDC token changed but bridge is busy.");
192+
SendSimpleResp(503, nullptr, nullptr, "Authentication busy", 0, false);
193+
return 1;
194+
}
195+
Bridge = nullptr;
196+
DoingLogin = false;
197+
DoneSetInfo = false;
198+
if (!oidcBearerTokKey.empty())
199+
TRACEI(REQ, " OIDC bearer token changed; re-authenticating.");
200+
}
201+
202+
if (SecEntity.name) free(SecEntity.name);
203+
SecEntity.name = strdup(identity.c_str());
204+
strncpy(SecEntity.prot, "oidc", sizeof(SecEntity.prot));
205+
for (const auto &attr : entityAttrs)
206+
SecEntity.eaAPI->Add(attr.first, attr.second, true);
207+
oidcBearerTokKey = tokKey;
208+
TRACEI(REQ, " OIDC authenticated as: " << SecEntity.name);
209+
return 0;
210+
}
211+
89212
/******************************************************************************/
90213
/* H a n d l e A u t h e n t i c a t i o n */
91214
/******************************************************************************/

src/XrdOuc/CMakeLists.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
find_package(CURL REQUIRED)
12
find_package(nlohmann_json 3.10.2 QUIET)
23

34
if(nlohmann_json_FOUND)
@@ -81,3 +82,16 @@ add_library(${XrdN2No2p} MODULE XrdOucN2No2p.cc)
8182
target_link_libraries(${XrdN2No2p} PRIVATE XrdUtils)
8283

8384
install(TARGETS ${XrdN2No2p} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
85+
86+
#-------------------------------------------------------------------------------
87+
# Shared OIDC JWT validation library
88+
#-------------------------------------------------------------------------------
89+
set(XrdOucOIDC XrdOucOIDC-${PLUGIN_VERSION})
90+
add_library(${XrdOucOIDC} SHARED XrdOucOIDC.cc)
91+
target_link_libraries(${XrdOucOIDC} PRIVATE XrdUtils OpenSSL::Crypto CURL::libcurl)
92+
set_target_properties(${XrdOucOIDC}
93+
PROPERTIES
94+
SOVERSION ${XRootD_VERSION_MAJOR}
95+
VERSION ${XRootD_LIBVERSION}
96+
)
97+
install(TARGETS ${XrdOucOIDC} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})

0 commit comments

Comments
 (0)