Skip to content

Commit 429de3a

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:Composer-2.5 CursorAI [XrdHttp] route http.oidc bearer auth through sec.protocol oidc. Delegate HTTPS bearer validation to the security framework via CIA so XrdHttp no longer links libXrdOucOIDC directly. Assisted-by: Cursor:Composer-2.5 CursorAI
1 parent 60477d5 commit 429de3a

21 files changed

Lines changed: 5706 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
@@ -63,6 +63,7 @@ add_subdirectory( XrdPosix )
6363
add_subdirectory( XrdSec )
6464
add_subdirectory( XrdSecgsi )
6565
add_subdirectory( XrdSeckrb5 )
66+
add_subdirectory( XrdSecoidc )
6667
add_subdirectory( XrdSecpwd )
6768
add_subdirectory( XrdSecsss )
6869
add_subdirectory( XrdSecunix )

src/XrdHttp/XrdHttpProtocol.cc

Lines changed: 35 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>
@@ -155,6 +156,9 @@ bool xrdctxVer = false;
155156

156157
using namespace XrdHttpProtoInfo;
157158

159+
int XrdHttpProtocol::oidcHttpMode = 0;
160+
const char *XrdHttpProtocol::oidcConfigFN = nullptr;
161+
158162
/******************************************************************************/
159163
/* P r o t o c o l M a n a g e m e n t S t a c k s */
160164
/******************************************************************************/
@@ -806,6 +810,11 @@ int XrdHttpProtocol::Process(XrdLink *lp) // We ignore the argument here
806810

807811

808812

813+
// Bearer OIDC authentication over HTTPS (via sec.protocol oidc / CIA).
814+
if (ishttps && ssldone && oidcHttpMode && HandleOidcAuthentication()) {
815+
return -1;
816+
}
817+
809818
// Now we have everything that is needed to try the login
810819
// Remember that if there is an exthandler then it has the responsibility
811820
// for authorization in the paths that it manages
@@ -904,6 +913,7 @@ int XrdHttpProtocol::Stats(char *buff, int blen, int do_sync) {
904913
eDest.Say("Config http." x " overrides the xrd." y " directive.")
905914

906915
int XrdHttpProtocol::Config(const char *ConfigFN, XrdOucEnv *myEnv) {
916+
XrdHttpProtocol::oidcConfigFN = ConfigFN;
907917
XrdOucEnv cfgEnv;
908918
XrdOucStream Config(&eDest, getenv("XRDINSTANCE"), &cfgEnv, "=====> ");
909919
std::vector<extHInfo> extHIVec;
@@ -1005,6 +1015,7 @@ int XrdHttpProtocol::Config(const char *ConfigFN, XrdOucEnv *myEnv) {
10051015
else if TS_Xeq("tlsreuse", xtlsreuse);
10061016
else if TS_Xeq("auth", xauth);
10071017
else if TS_Xeq("tlsclientauth", xtlsclientauth);
1018+
else if TS_Xeq("oidc", xoidc);
10081019
else if TS_Xeq("maxdelay", xmaxdelay);
10091020
else {
10101021
eDest.Say("Config warning: ignoring unknown directive '", var, "'.");
@@ -1990,6 +2001,7 @@ void XrdHttpProtocol::Reset() {
19902001
ishttps = false;
19912002
ssldone = false;
19922003

2004+
oidcBearerTokKey.clear();
19932005
Bridge = 0;
19942006
ssl = 0;
19952007
sbio = 0;
@@ -3001,6 +3013,29 @@ int XrdHttpProtocol::xtlsclientauth(XrdOucStream &Config) {
30013013
return 1;
30023014
}
30033015

3016+
int XrdHttpProtocol::xoidc(XrdOucStream &Config) {
3017+
char *val = Config.GetWord();
3018+
if (!val || !val[0])
3019+
{eDest.Emsg("Config", "http.oidc argument not specified"); return 1;}
3020+
3021+
if (!strcmp(val, "on") || !strcmp(val, "optional"))
3022+
{oidcHttpMode = 1;
3023+
return 0;
3024+
}
3025+
if (!strcmp(val, "require"))
3026+
{oidcHttpMode = 2;
3027+
return 0;
3028+
}
3029+
if (val[0] == '-')
3030+
{eDest.Emsg("Config", "http.oidc inline parameters are not supported;",
3031+
"configure OIDC via sec.protparm oidc and sec.protocol oidc");
3032+
return 1;
3033+
}
3034+
3035+
eDest.Emsg("Config", "invalid http.oidc parameter -", val);
3036+
return 1;
3037+
}
3038+
30043039
int XrdHttpProtocol::xauth(XrdOucStream &Config) {
30053040
char *val = Config.GetWord();
30063041
if(val) {

src/XrdHttp/XrdHttpProtocol.hh

Lines changed: 14 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,15 @@ 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+
242+
/// Config file path used to load the security framework for http.oidc.
243+
static const char *oidcConfigFN;
244+
234245
static bool isRequiredXtractor; // If true treat secxtractor errors as fatal
235246
static XrdHttpSecXtractor *secxtractor;
236247

@@ -376,7 +387,9 @@ protected:
376387
/// The Bridge that we use to exercise the xrootd internals
377388
XrdXrootd::Bridge *Bridge;
378389

379-
390+
/// SHA-256 fingerprint of the validated OIDC bearer token on this connection.
391+
std::string oidcBearerTokKey;
392+
380393
/// Area for coordinating request and responses to/from the bridge
381394
/// This also can process HTTP/DAV stuff
382395
XrdHttpReq CurrentReq;

src/XrdHttp/XrdHttpSecurity.cc

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

22+
#include <cctype>
23+
#include <cstdio>
24+
#include <cstring>
25+
#include <map>
26+
#include <string>
27+
#include <vector>
28+
29+
#include <openssl/evp.h>
30+
2231
#include "XrdHttpProtocol.hh"
2332
#include "XrdHttpTrace.hh"
2433
#include "XrdHttpSecXtractor.hh"
34+
#include "XrdSec/XrdSecLoadSecurity.hh"
35+
#include "XrdSec/XrdSecInterface.hh"
36+
#include "XrdOuc/XrdOucErrInfo.hh"
2537
#include "Xrd/XrdLink.hh"
2638
#include "XrdCrypto/XrdCryptoX509Chain.hh"
2739
#include "XrdCrypto/XrdCryptosslAux.hh"
@@ -49,6 +61,81 @@ const char *TraceID = "Security";
4961

5062
using namespace XrdHttpProtoInfo;
5163

64+
namespace
65+
{
66+
67+
std::string bearerTokenKey(const char *tok, int tlen)
68+
{
69+
unsigned char md[EVP_MAX_MD_SIZE];
70+
unsigned int mdLen = 0;
71+
if (!tok || tlen <= 0
72+
|| !EVP_Digest(tok, static_cast<size_t>(tlen), md, &mdLen, EVP_sha256(), nullptr))
73+
return {};
74+
char hex[EVP_MAX_MD_SIZE * 2 + 1];
75+
for (unsigned int i = 0; i < mdLen; ++i)
76+
snprintf(hex + i * 2, sizeof(hex) - i * 2, "%02x", md[i]);
77+
return std::string(hex, mdLen * 2);
78+
}
79+
80+
bool hasPrefix(const char *s, const char *end, const char *pfx)
81+
{
82+
while (*pfx && s < end && *s == *pfx) {++s; ++pfx;}
83+
return !*pfx;
84+
}
85+
86+
const char *stripBearerToken(const char *bTok, int &sz)
87+
{
88+
const char *sTok = bTok;
89+
sz = 0;
90+
if (!sTok) return nullptr;
91+
92+
const char *endPtr = sTok + strlen(sTok);
93+
while (sTok < endPtr && isspace(static_cast<unsigned char>(*sTok))) ++sTok;
94+
if (sTok >= endPtr) return nullptr;
95+
96+
if ((endPtr - sTok) >= 9 && hasPrefix(sTok, endPtr, "Bearer%20")) sTok += 9;
97+
else if ((endPtr - sTok) >= 7 && hasPrefix(sTok, endPtr, "Bearer ")) sTok += 7;
98+
99+
while (sTok < endPtr && isspace(static_cast<unsigned char>(*sTok))) ++sTok;
100+
if (sTok >= endPtr) return nullptr;
101+
102+
while (endPtr > sTok && isspace(static_cast<unsigned char>(*(endPtr - 1)))) --endPtr;
103+
sz = static_cast<int>(endPtr - sTok);
104+
return (sz > 0 ? sTok : nullptr);
105+
}
106+
107+
void copyEntityAttrs(XrdSecEntity &dst, const XrdSecEntity &src)
108+
{
109+
for (const auto &key : src.eaAPI->Keys())
110+
{std::string val;
111+
if (src.eaAPI->Get(key, val)) dst.eaAPI->Add(key, val, true);
112+
}
113+
}
114+
115+
bool extractBearerToken(const XrdHttpReq &req, std::string &token)
116+
{
117+
const std::string &cgi = req.hdr2cgistr;
118+
const char *key = "authz=";
119+
size_t pos = cgi.find(key);
120+
if (pos != std::string::npos)
121+
{size_t start = pos + strlen(key);
122+
size_t end = cgi.find('&', start);
123+
token = (end == std::string::npos ? cgi.substr(start)
124+
: cgi.substr(start, end - start));
125+
return !token.empty();
126+
}
127+
128+
for (const auto &hdr : req.allheaders)
129+
{if (!strcasecmp(hdr.first.c_str(), "authorization"))
130+
{token = hdr.second;
131+
return !token.empty();
132+
}
133+
}
134+
return false;
135+
}
136+
137+
} // namespace
138+
52139
/******************************************************************************/
53140
/* I n i t S e c u r i t y */
54141
/******************************************************************************/
@@ -81,11 +168,125 @@ bool XrdHttpProtocol::InitSecurity() {
81168
secxtractor->Init(sslctx, XrdHttpTrace.What);
82169
}
83170

171+
// Load the security framework when HTTP bearer OIDC is enabled.
172+
//
173+
if (oidcHttpMode && !CIA)
174+
{if (!oidcConfigFN)
175+
{eDest.Say("Error: http.oidc requires a configuration file path");
176+
return false;
177+
}
178+
if (!(CIA = XrdSecLoadSecService(&eDest, oidcConfigFN)))
179+
{eDest.Say("Error loading security framework for http.oidc");
180+
return false;
181+
}
182+
}
183+
84184
// All done
85185
//
86186
return true;
87187
}
88188

189+
/******************************************************************************/
190+
/* H a n d l e O I D C A u t h e n t i c a t i o n */
191+
/******************************************************************************/
192+
193+
int
194+
XrdHttpProtocol::HandleOidcAuthentication()
195+
{
196+
#undef TRACELINK
197+
#define TRACELINK Link
198+
199+
if (!oidcHttpMode || !CIA) return 0;
200+
201+
// Client-certificate identity is fixed for the TLS connection.
202+
if (SecEntity.name && strncmp(SecEntity.prot, "oidc", 4) != 0) return 0;
203+
204+
std::string bearer;
205+
if (!extractBearerToken(CurrentReq, bearer))
206+
{if (oidcHttpMode == 2 && oidcBearerTokKey.empty())
207+
{TRACEI(REQ, " OIDC bearer token required but not provided.");
208+
SendSimpleResp(401, nullptr, nullptr, "Authentication required", 0, false);
209+
return 1;
210+
}
211+
return 0;
212+
}
213+
214+
int tlen = 0;
215+
const char *tok = stripBearerToken(bearer.c_str(), tlen);
216+
if (!tok || tlen <= 0)
217+
{TRACEI(REQ, " OIDC bearer token malformed.");
218+
SendSimpleResp(401, nullptr, nullptr, "Authentication failed", 0, false);
219+
return 1;
220+
}
221+
222+
const std::string tokKey = bearerTokenKey(tok, tlen);
223+
if (tokKey.empty())
224+
{TRACEI(REQ, " OIDC bearer token fingerprint failed.");
225+
SendSimpleResp(500, nullptr, nullptr, "Authentication failed", 0, false);
226+
return 1;
227+
}
228+
229+
if (!oidcBearerTokKey.empty() && tokKey == oidcBearerTokKey) return 0;
230+
231+
const int bsz = 5 + tlen + 1;
232+
std::vector<char> credBuf(static_cast<size_t>(bsz));
233+
strcpy(credBuf.data(), "oidc");
234+
memcpy(credBuf.data() + 5, tok, static_cast<size_t>(tlen));
235+
credBuf[static_cast<size_t>(5 + tlen)] = '\0';
236+
237+
XrdSecCredentials cred;
238+
cred.buffer = credBuf.data();
239+
cred.size = bsz;
240+
241+
XrdOucErrInfo eMsg;
242+
XrdSecProtocol *authProt = CIA->getProtocol(Link->Host(), *(Link->AddrInfo()),
243+
&cred, eMsg);
244+
if (!authProt)
245+
{int ec = 0;
246+
const char *et = eMsg.getErrText(ec);
247+
TRACEI(REQ, " OIDC protocol unavailable: " << (et && *et ? et : "unknown"));
248+
SendSimpleResp(401, nullptr, nullptr, "Authentication failed", 0, false);
249+
return 1;
250+
}
251+
252+
XrdSecParameters *parm = nullptr;
253+
const int rc = authProt->Authenticate(&cred, &parm, &eMsg);
254+
if (parm) delete parm;
255+
256+
if (rc != 0 || !CIA->PostProcess(authProt->Entity, eMsg))
257+
{int ec = 0;
258+
const char *et = eMsg.getErrText(ec);
259+
TRACEI(REQ, " OIDC token validation failed: " << (et && *et ? et : "unknown"));
260+
authProt->Delete();
261+
SendSimpleResp(401, nullptr, nullptr, "Authentication failed", 0, false);
262+
return 1;
263+
}
264+
265+
if (!oidcBearerTokKey.empty() || Bridge)
266+
{if (Bridge && !Bridge->Disc())
267+
{TRACEI(REQ, " OIDC token changed but bridge is busy.");
268+
authProt->Delete();
269+
SendSimpleResp(503, nullptr, nullptr, "Authentication busy", 0, false);
270+
return 1;
271+
}
272+
Bridge = nullptr;
273+
DoingLogin = false;
274+
DoneSetInfo = false;
275+
if (!oidcBearerTokKey.empty())
276+
TRACEI(REQ, " OIDC bearer token changed; re-authenticating.");
277+
}
278+
279+
if (SecEntity.name) free(SecEntity.name);
280+
SecEntity.name = authProt->Entity.name ? strdup(authProt->Entity.name) : nullptr;
281+
strncpy(SecEntity.prot, authProt->Entity.prot, sizeof(SecEntity.prot));
282+
copyEntityAttrs(SecEntity, authProt->Entity);
283+
authProt->Delete();
284+
285+
oidcBearerTokKey = tokKey;
286+
TRACEI(REQ, " OIDC authenticated as: " << SecEntity.name);
287+
return 0;
288+
}
289+
89290
/******************************************************************************/
90291
/* H a n d l e A u t h e n t i c a t i o n */
91292
/******************************************************************************/

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)