|
| 1 | +# XrdSecOIDC |
| 2 | + |
| 3 | +`XrdSecOIDC` is an XRootD security protocol plugin (`sec.protocol oidc`) that |
| 4 | +authenticates bearer-style OIDC tokens over TLS. |
| 5 | + |
| 6 | +This implementation performs OIDC-style JWT validation directly in the OIDC |
| 7 | +plugin (no SciTokens helper dependency). |
| 8 | + |
| 9 | +## Client token pickup order |
| 10 | + |
| 11 | +The client side `getCredentials()` searches for a token in this order: |
| 12 | + |
| 13 | +1. `XRD_SSO_TOKEN` (raw token value) |
| 14 | +2. `XRD_SSO_TOKEN_FILE` (path to token file) |
| 15 | +3. `BEARER_TOKEN` (raw token value, compatibility fallback) |
| 16 | +4. `BEARER_TOKEN_FILE` (path to token file, compatibility fallback) |
| 17 | +5. `XDG_RUNTIME_DIR/bt_u<uid>` |
| 18 | +6. `/tmp/bt_u<uid>` |
| 19 | + |
| 20 | +If no token is found, authentication fails with `ENOPROTOOPT`. |
| 21 | + |
| 22 | +When a token file is used (`*_TOKEN_FILE`, `XDG_RUNTIME_DIR/bt_u<uid>`, |
| 23 | +`/tmp/bt_u<uid>`), it must be: |
| 24 | + |
| 25 | +- a regular file (no device/FIFO/etc.), |
| 26 | +- owned by the effective client uid, |
| 27 | +- accessible by owner only (no group/other permission bits). |
| 28 | + |
| 29 | +## Server initialization parameters |
| 30 | + |
| 31 | +`XrdSecProtocoloidcInit()` supports: |
| 32 | + |
| 33 | +- `-maxsz <num>`: maximum token size (default 8192, max 524288) |
| 34 | +- `-expiry {ignore|optional|required}`: |
| 35 | + - `ignore` = do not enforce expiry claim |
| 36 | + - `optional` = enforce only if expiry present |
| 37 | + - `required` = expiry must be present and valid |
| 38 | +- `-issuer <url>`: expected `iss` claim value; if `-oidc-config-url` is not |
| 39 | + specified, discovery URL defaults to `<issuer>/.well-known/openid-configuration` |
| 40 | +- `-audience <value>`: expected token audience (`aud` string or array member); |
| 41 | + may be repeated for the current issuer |
| 42 | +- `-oidc-config-url <https-url>`: OpenID discovery URL (used to locate JWKS URI) |
| 43 | +- `-jwks-url <https-url>`: explicit JWKS endpoint (overrides discovery lookup) |
| 44 | +- `-jwks-refresh <seconds>`: JWKS refresh cache interval (default 300) |
| 45 | +- `-jwks-cache-file <path>`: optional on-disk JWKS cache file shared across |
| 46 | + issuers (disabled by default) |
| 47 | +- `-jwks-cache-ttl <seconds>`: TTL for on-disk cached issuer keys |
| 48 | + (`0` = use `-jwks-refresh`; default 0) |
| 49 | +- `-clock-skew <seconds>`: allowed clock skew for time-based claims (default 60) |
| 50 | +- `-identity-claim <claim>`: add claim names (in order) used for user identity; |
| 51 | + may be specified multiple times |
| 52 | +- `-forced-identity-claim <claim>`: for the current `-issuer`, force identity |
| 53 | + extraction to this single claim (no fallback order for that issuer) |
| 54 | + - special case: when set to `email`, the email value is mapped to local |
| 55 | + username via `[email-map]` entries in `/etc/xrootd/oidc.cfg` |
| 56 | +- `-debug-token`: print decoded JWT header/payload on successful auth (debug only; |
| 57 | + contains sensitive information) |
| 58 | +- `-show-token-claims`: print selected claims only (`alg`, `kid`, `typ`, `iss`, |
| 59 | + `aud`, `sub`, `preferred_username`, `azp`, `iat`, `nbf`, `exp`) |
| 60 | +- `-token-cache-max <num>`: max number of cached validated tokens (default 10000; |
| 61 | + set `0` to disable caching) |
| 62 | +- `-token-cache-noexp-ttl <seconds>`: cache TTL for tokens that do not include |
| 63 | + `exp` when `-expiry optional|ignore` (default 60) |
| 64 | + |
| 65 | +If no inline parameters are supplied on `sec.protocol oidc`, the plugin |
| 66 | +automatically tries to load `/etc/xrootd/oidc.cfg` (INI-style) and maps keys to |
| 67 | +the same options listed above. |
| 68 | + |
| 69 | +`-issuer` starts a new issuer policy block. `-audience`, `-oidc-config-url`, and |
| 70 | +`-jwks-url` that follow apply to that issuer until the next `-issuer`. |
| 71 | +`-forced-identity-claim` is also issuer-scoped. |
| 72 | + |
| 73 | +## TLS requirement |
| 74 | + |
| 75 | +`oidc` rejects non-TLS connections. Both client and server constructors enforce |
| 76 | +TLS-only use. |
| 77 | + |
| 78 | +## Server-side identity mapping |
| 79 | + |
| 80 | +After signature and claim validation, the server sets `XrdSecEntity.name` from |
| 81 | +claims in this default order: |
| 82 | + |
| 83 | +1. `preferred_username` |
| 84 | +2. `upn` |
| 85 | +3. `username` |
| 86 | +4. `name` |
| 87 | +5. `sub` |
| 88 | + |
| 89 | +Use repeated `-identity-claim` options to override this order. |
| 90 | + |
| 91 | +Per-issuer override: use `-forced-identity-claim <claim>` after a specific |
| 92 | +`-issuer` to force a single claim for that issuer. |
| 93 | +If `forced-identity-claim = email` is used, authentication fails unless the |
| 94 | +token has an `email` claim and that email is present in `[email-map]`. |
| 95 | + |
| 96 | +## Example config snippet |
| 97 | + |
| 98 | +```conf |
| 99 | +sec.protocol oidc \ |
| 100 | + -issuer https://issuer-a.example \ |
| 101 | + -audience xrootd \ |
| 102 | + -audience xrootd-admin \ |
| 103 | + -issuer https://issuer-b.example \ |
| 104 | + -audience service-b \ |
| 105 | + -expiry required |
| 106 | +``` |
| 107 | + |
| 108 | +## Standard CERN SSO configuration |
| 109 | + |
| 110 | +Inline `xrootd.cf` style: |
| 111 | + |
| 112 | +```conf |
| 113 | +sec.protocol oidc \ |
| 114 | + -issuer https://auth.cern.ch/auth/realms/cern \ |
| 115 | + -audience public-client \ |
| 116 | + -expiry required \ |
| 117 | + -show-token-claims |
| 118 | +``` |
| 119 | + |
| 120 | +INI fallback (`/etc/xrootd/oidc.cfg`) equivalent: |
| 121 | + |
| 122 | +```ini |
| 123 | +[global] |
| 124 | +expiry = required |
| 125 | +show-token-claims = true |
| 126 | +# issuer configuration |
| 127 | +``` |
| 128 | + |
| 129 | +## Standard CERN configuration |
| 130 | +Example `oidc.cfg` using CERN issuer and default mapping: |
| 131 | +```ini |
| 132 | +[issuer "https://auth.cern.ch/auth/realms/cern"] |
| 133 | +audience = public-client |
| 134 | +``` |
| 135 | + |
| 136 | +## Standard Google configuration |
| 137 | + |
| 138 | +Example `oidc.cfg` using Google issuer + email mapping: |
| 139 | + |
| 140 | +```ini |
| 141 | +[issuer "https://accounts.google.com"] |
| 142 | +audience = 780271439668-1aukl5va8p6rbf5i81sgpg5ppr8s2p63.apps.googleusercontent.com |
| 143 | +forced-identity-claim = email |
| 144 | + |
| 145 | +[email-map] |
| 146 | +foo.bar@gmail.com = foo |
| 147 | +``` |
| 148 | + |
| 149 | +Google app setup (Google Cloud Console): |
| 150 | + |
| 151 | +1. Create/select a project. |
| 152 | +2. Configure OAuth consent screen. |
| 153 | +3. Create OAuth client credentials: |
| 154 | + - for `xrdsso ... GOOGLE` default flow (`--flow device`), use a client type |
| 155 | + that supports device authorization and provide both `client_id` and |
| 156 | + `client_secret` to `xrdsso`. |
| 157 | + - for `--flow pkce`, use a Desktop app client id. |
| 158 | +4. Copy client id/secret and use them in `xrdsso` options (or env vars). |
| 159 | + |
| 160 | +Quickstart (Google): |
| 161 | + |
| 162 | +```sh |
| 163 | +# 0) Minimal server config (example: /etc/xrootd/xrootd.cf) |
| 164 | +cat > /etc/xrootd/xrootd.cf <<'EOF' |
| 165 | +########################################################### |
| 166 | +xrootd.seclib libXrdSec.so |
| 167 | +all.role server |
| 168 | +sec.protocol oidc |
| 169 | +xrootd.tls all |
| 170 | +xrd.tlsca certdir /etc/grid-security/certificates |
| 171 | +xrd.tls /etc/grid-security/xrd/xrdcert.pem /etc/grid-security/xrd/xrdkey.pem |
| 172 | +EOF |
| 173 | + |
| 174 | +# 0b) Start the server (foreground example) |
| 175 | +xrootd -c /etc/xrootd/xrootd.cf -R xrootd |
| 176 | + |
| 177 | +# 1) Create a token (Google device flow) |
| 178 | +./utils/xrdsso create ./build/google.token GOOGLE \ |
| 179 | + --client-id "<google-oauth-client-id>" \ |
| 180 | + --client-secret "<google-oauth-client-secret>" |
| 181 | + |
| 182 | +# 2) Inspect token claims |
| 183 | +./utils/xrdsso show ./build/google.token |
| 184 | + |
| 185 | +# 3) Try access with xrdfs |
| 186 | +XRD_SSO_TOKEN_FILE="$PWD/build/google.token" \ |
| 187 | +LD_LIBRARY_PATH=/usr/local/lib64 \ |
| 188 | +/usr/local/bin/xrdfs localhost:2000 stat /tmp/ |
| 189 | +``` |
| 190 | + |
| 191 | +## INI fallback file (`/etc/xrootd/oidc.cfg`) |
| 192 | + |
| 193 | +When `sec.protocol oidc` has no trailing parameters, this file is required and |
| 194 | +loaded at plugin init time. If the file is missing, initialization fails. |
| 195 | + |
| 196 | +```ini |
| 197 | +[global] |
| 198 | +maxsz = 8192 |
| 199 | +expiry = required |
| 200 | +jwks-refresh = 300 |
| 201 | +jwks-cache-file = /var/lib/xrootd/oidc-jwks-cache.ini |
| 202 | +jwks-cache-ttl = 600 |
| 203 | +clock-skew = 60 |
| 204 | +identity-claim = preferred_username,sub |
| 205 | +show-token-claims = true |
| 206 | +token-cache-max = 10000 |
| 207 | +token-cache-noexp-ttl = 60 |
| 208 | + |
| 209 | +[issuer "https://issuer-a.example"] |
| 210 | +audience = xrootd,xrootd-admin |
| 211 | +forced-identity-claim = preferred_username |
| 212 | +# Optional if you want to override discovery default: |
| 213 | +# oidc-config-url = https://issuer-a.example/.well-known/openid-configuration |
| 214 | +# jwks-url = https://issuer-a.example/protocol/openid-connect/certs |
| 215 | + |
| 216 | +[issuer "https://issuer-b.example"] |
| 217 | +audience = service-b |
| 218 | + |
| 219 | +[email-map] |
| 220 | +alice@example.org = alice |
| 221 | +bob@example.org = bobby |
| 222 | +``` |
| 223 | + |
| 224 | +Supported sections are `[global]` and `[issuer "<issuer-url>"]` (or `[issuer]` |
| 225 | +with `issuer = <url>` inside the section), plus `[email-map]` for |
| 226 | +`forced-identity-claim = email`. Boolean keys accept `true/false`, `yes/no`, |
| 227 | +`on/off`, `1/0`. |
| 228 | + |
| 229 | +Security checks for `/etc/xrootd/oidc.cfg`: |
| 230 | + |
| 231 | +- must be a regular file, |
| 232 | +- must be owned by the effective uid of the running xrootd process, |
| 233 | +- must not be writable by group or others. |
| 234 | + |
| 235 | +When `jwks-cache-file` is configured, cached keys are stored per issuer in that |
| 236 | +file and reused across refresh/restart cycles until TTL expiry. |
| 237 | + |
| 238 | +## Notes |
| 239 | + |
| 240 | +- Protocol id on the wire is `oidc`. |
| 241 | +- Accepted JWT signature algorithm is currently `RS256`. |
| 242 | +- OIDC discovery and JWKS endpoints must use `https://`. |
| 243 | +- Server-side token cache is enabled by default and keyed by raw token value. |
| 244 | + |
| 245 | +## Local helper scripts |
| 246 | + |
| 247 | +For quick manual testing, helper scripts are available in `utils/`: |
| 248 | + |
| 249 | +- `utils/xrdsso create [tokenfile] [CERN|CERNOIDC|GOOGLE]`: creates token |
| 250 | + (`CERN` uses OAuth2 access token flow, `CERNOIDC` requests OIDC scopes and |
| 251 | + requires/stores `id_token`, `GOOGLE` defaults to device flow). |
| 252 | +- Shortcut: `utils/xrdsso CERNOIDC [tokenfile]` is equivalent to |
| 253 | + `utils/xrdsso create [tokenfile] CERNOIDC`. |
| 254 | +- `utils/xrdsso show [tokenfile]`: decodes and prints JWT header/payload. |
| 255 | +- If `<tokenfile>` is omitted, default is `${XDG_RUNTIME_DIR}/bt_u<uid>` |
| 256 | + (or `/tmp/bt_u<uid>` when `XDG_RUNTIME_DIR` is unset). |
| 257 | +- For `GOOGLE`, provide `--client-id` (or set `GOOGLE_OAUTH_CLIENT_ID`). |
| 258 | +- For `GOOGLE` default device flow, also provide `--client-secret` (or set |
| 259 | + `GOOGLE_OAUTH_CLIENT_SECRET`). |
| 260 | +- `GOOGLE` flow can be selected via `--flow device|pkce` (`device` default). |
| 261 | +- `--client-secret` is optional for PKCE but may be required for some client |
| 262 | + types/endpoints; env fallback: `GOOGLE_OAUTH_CLIENT_SECRET`. |
| 263 | +- For `GOOGLE`, `xrdsso` stores `id_token` when present (JWT), otherwise |
| 264 | + falls back to `access_token`. |
| 265 | +- Device flow prints a prefilled verification URL when possible; if your browser |
| 266 | + still asks, use the printed device code. |
| 267 | + |
| 268 | +Example: |
| 269 | + |
| 270 | +```sh |
| 271 | +./utils/xrdsso create |
| 272 | +./utils/xrdsso CERNOIDC |
| 273 | +./utils/xrdsso create GOOGLE \ |
| 274 | + --client-id "<google-oauth-client-id>" \ |
| 275 | + --client-secret "<google-oauth-client-secret>" |
| 276 | +./utils/xrdsso create GOOGLE --flow pkce \ |
| 277 | + --client-id "<google-oauth-client-id>" |
| 278 | +./utils/xrdsso show |
| 279 | +``` |
0 commit comments