@@ -22,7 +22,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
2222from pathlib import Path
2323from typing import Any , Optional
2424
25- PROVIDERS = frozenset ({"CERN" , "CERNOIDC" , "GOOGLE" , "GITHUB" })
25+ PROVIDERS = frozenset ({"CERN" , "CERNOIDC" , "GOOGLE" , "GITHUB" , "IAM" , "WLCG" })
2626
2727
2828@dataclass
@@ -38,7 +38,7 @@ class ProviderSettings:
3838
3939def usage () -> str :
4040 return """Usage:
41- xrdtoken create [tokenfile] [CERN|CERNOIDC|GOOGLE|GITHUB] [--provider CERN|CERNOIDC|GOOGLE|GITHUB] [--flow device|pkce] [--client-id ID] [--client-secret SECRET] [--scope SCOPE] [--auth-ep URL] [--device-ep URL] [--token-ep URL]
41+ xrdtoken create [tokenfile] [CERN|CERNOIDC|GOOGLE|GITHUB|IAM|WLCG ] [--provider CERN|CERNOIDC|GOOGLE|GITHUB|IAM|WLCG ] [--flow device|pkce] [--issuer URL ] [--client-id ID] [--client-secret SECRET] [--scope SCOPE] [--auth-ep URL] [--device-ep URL] [--token-ep URL]
4242 xrdtoken show [tokenfile]
4343
4444Defaults:
@@ -71,6 +71,14 @@ Defaults:
7171 client-id : must be provided (enable Device flow in the OAuth app settings)
7272 client-secret : not required for device flow; required for --flow pkce
7373 stored token : access_token (opaque GitHub OAuth token, not a JWT)
74+ IAM / WLCG (aliases):
75+ flow : device
76+ issuer : https://wlcg.cloud.cnaf.infn.it/ (override with --issuer or WLCG_IAM_ISSUER)
77+ device-ep : <issuer>/devicecode
78+ token-ep : <issuer>/token
79+ scope : required WLCG storage scopes (e.g. storage.read:/public storage.modify:/alice)
80+ client-id : must be provided (or WLCG_IAM_CLIENT_ID / OIDC_CLIENT_ID)
81+ stored token : access_token (WLCG-profile JWT for XrdSecoidc + XrdAccToken)
7482 tokenfile : ${XDG_RUNTIME_DIR}/bt_u<uid> (fallback: /tmp/bt_u<uid>)"""
7583
7684
@@ -87,8 +95,39 @@ def default_tokenfile() -> Path:
8795 return Path (f"/tmp/bt_u{ uid } " )
8896
8997
98+ def iam_issuer_base (issuer : Optional [str ] = None ) -> str :
99+ base = (
100+ issuer
101+ or os .environ .get ("WLCG_IAM_ISSUER" )
102+ or os .environ .get ("OIDC_ISSUER" )
103+ or "https://wlcg.cloud.cnaf.infn.it/"
104+ )
105+ return base .rstrip ("/" )
106+
107+
108+ def endpoints_from_issuer (issuer : str ) -> tuple [str , str , str ]:
109+ base = iam_issuer_base (issuer )
110+ return (
111+ f"{ base } /devicecode" ,
112+ f"{ base } /token" ,
113+ f"{ base } /authorize" ,
114+ )
115+
116+
90117def provider_defaults (provider : str ) -> ProviderSettings :
91118 provider = provider .upper ()
119+ if provider in {"IAM" , "WLCG" }:
120+ device_ep , token_ep , auth_ep = endpoints_from_issuer ("" )
121+ return ProviderSettings (
122+ client_id = os .environ .get ("WLCG_IAM_CLIENT_ID" )
123+ or os .environ .get ("OIDC_CLIENT_ID" )
124+ or "" ,
125+ flow = "device" ,
126+ scope = "" ,
127+ auth_ep = auth_ep ,
128+ device_ep = device_ep ,
129+ token_ep = token_ep ,
130+ )
92131 if provider == "CERN" :
93132 return ProviderSettings (
94133 client_id = "public-client" ,
@@ -120,7 +159,7 @@ def provider_defaults(provider: str) -> ProviderSettings:
120159 token_ep = "https://github.com/login/oauth/access_token" ,
121160 scope = "read:user" ,
122161 )
123- die (f"unsupported provider '{ provider } ' (expected CERN, CERNOIDC, GOOGLE, or GITHUB )" )
162+ die (f"unsupported provider '{ provider } ' (expected CERN, CERNOIDC, GOOGLE, GITHUB, IAM, or WLCG )" )
124163
125164
126165def parse_oauth_response (body : str ) -> dict [str , Any ]:
@@ -194,6 +233,8 @@ def chosen_token(provider: str, token_resp: dict[str, Any]) -> str:
194233 id_token = token_resp .get ("id_token" ) or ""
195234 if provider == "CERNOIDC" :
196235 return id_token
236+ if provider in {"IAM" , "WLCG" }:
237+ return access_token
197238 if provider == "GOOGLE" and id_token :
198239 return id_token
199240 return access_token
@@ -224,6 +265,7 @@ def parse_create_options(argv: list[str], provider: str) -> tuple[ProviderSettin
224265 parser = argparse .ArgumentParser (add_help = False )
225266 parser .add_argument ("--provider" )
226267 parser .add_argument ("--flow" )
268+ parser .add_argument ("--issuer" )
227269 parser .add_argument ("--client-id" )
228270 parser .add_argument ("--client-secret" )
229271 parser .add_argument ("--scope" )
@@ -239,6 +281,11 @@ def parse_create_options(argv: list[str], provider: str) -> tuple[ProviderSettin
239281 provider = args .provider .upper ()
240282 if args .flow :
241283 settings .flow = args .flow .lower ()
284+ if args .issuer :
285+ device_ep , token_ep , auth_ep = endpoints_from_issuer (args .issuer )
286+ settings .device_ep = device_ep
287+ settings .token_ep = token_ep
288+ settings .auth_ep = auth_ep
242289 if args .client_id is not None :
243290 settings .client_id = args .client_id
244291 if args .client_secret is not None :
@@ -442,9 +489,24 @@ def cmd_create(argv: list[str]) -> None:
442489 if settings .flow == "pkce" and not settings .client_secret :
443490 die ("GITHUB pkce flow requires --client-secret (or GITHUB_OAUTH_CLIENT_SECRET)" )
444491
445- if provider in {"CERN" , "CERNOIDC" } and settings .flow != "device" :
492+ if provider in {"CERN" , "CERNOIDC" , "IAM" , "WLCG" } and settings .flow != "device" :
446493 die (f"{ provider } provider currently supports only --flow device" )
447494
495+ if provider in {"IAM" , "WLCG" }:
496+ if not settings .client_id :
497+ settings .client_id = (
498+ os .environ .get ("WLCG_IAM_CLIENT_ID" )
499+ or os .environ .get ("OIDC_CLIENT_ID" )
500+ or ""
501+ )
502+ if not settings .client_id :
503+ die ("IAM/WLCG provider requires --client-id (or WLCG_IAM_CLIENT_ID)" )
504+ if not settings .scope :
505+ die (
506+ "IAM/WLCG provider requires --scope with WLCG storage scopes "
507+ "(e.g. 'storage.read:/public storage.modify:/alice')"
508+ )
509+
448510 if provider in {"GOOGLE" , "GITHUB" } and settings .flow == "pkce" :
449511 token_resp = pkce_flow (provider , settings )
450512 else :
@@ -467,6 +529,8 @@ def cmd_create(argv: list[str]) -> None:
467529 print ("Stored token type: access_token (non-JWT possible)" )
468530 if provider == "CERNOIDC" :
469531 print ("Stored token type: id_token (JWT)" )
532+ if provider in {"IAM" , "WLCG" }:
533+ print ("Stored token type: access_token (WLCG-profile JWT)" )
470534 if provider == "GITHUB" :
471535 print ("Stored token type: access_token (GitHub OAuth token, not a JWT)" )
472536 expires_in = token_resp .get ("expires_in" )
0 commit comments