1# NixOS module for oauth2_proxy.
2
3{ config, lib, pkgs, ... }:
4
5with lib;
6let
7 cfg = config.services.oauth2_proxy;
8
9 # oauth2_proxy provides many options that are only relevant if you are using
10 # a certain provider. This set maps from provider name to a function that
11 # takes the configuration and returns a string that can be inserted into the
12 # command-line to launch oauth2_proxy.
13 providerSpecificOptions = {
14 azure = cfg: {
15 azure-tenant = cfg.azure.tenant;
16 resource = cfg.azure.resource;
17 };
18
19 github = cfg: { github = {
20 inherit (cfg.github) org team;
21 }; };
22
23 google = cfg: { google = with cfg.google; optionalAttrs (groups != []) {
24 admin-email = adminEmail;
25 service-account = serviceAccountJSON;
26 group = groups;
27 }; };
28 };
29
30 authenticatedEmailsFile = pkgs.writeText "authenticated-emails" cfg.email.addresses;
31
32 getProviderOptions = cfg: provider: providerSpecificOptions.${provider} or (_: {}) cfg;
33
34 allConfig = with cfg; {
35 inherit (cfg) provider scope upstream;
36 approval-prompt = approvalPrompt;
37 basic-auth-password = basicAuthPassword;
38 client-id = clientID;
39 client-secret = clientSecret;
40 custom-templates-dir = customTemplatesDir;
41 email-domain = email.domains;
42 http-address = httpAddress;
43 login-url = loginURL;
44 pass-access-token = passAccessToken;
45 pass-basic-auth = passBasicAuth;
46 pass-host-header = passHostHeader;
47 reverse-proxy = reverseProxy;
48 proxy-prefix = proxyPrefix;
49 profile-url = profileURL;
50 redeem-url = redeemURL;
51 redirect-url = redirectURL;
52 request-logging = requestLogging;
53 skip-auth-regex = skipAuthRegexes;
54 signature-key = signatureKey;
55 validate-url = validateURL;
56 htpasswd-file = htpasswd.file;
57 cookie = {
58 inherit (cookie) domain secure expire name secret refresh;
59 httponly = cookie.httpOnly;
60 };
61 set-xauthrequest = setXauthrequest;
62 } // lib.optionalAttrs (cfg.email.addresses != null) {
63 authenticated-emails-file = authenticatedEmailsFile;
64 } // lib.optionalAttrs (cfg.passBasicAuth) {
65 basic-auth-password = cfg.basicAuthPassword;
66 } // lib.optionalAttrs (cfg.htpasswd.file != null) {
67 display-htpasswd-file = cfg.htpasswd.displayForm;
68 } // lib.optionalAttrs tls.enable {
69 tls-cert-file = tls.certificate;
70 tls-key-file = tls.key;
71 https-address = tls.httpsAddress;
72 } // (getProviderOptions cfg cfg.provider) // cfg.extraConfig;
73
74 mapConfig = key: attr:
75 optionalString (attr != null && attr != []) (
76 if isDerivation attr then mapConfig key (toString attr) else
77 if (builtins.typeOf attr) == "set" then concatStringsSep " "
78 (mapAttrsToList (name: value: mapConfig (key + "-" + name) value) attr) else
79 if (builtins.typeOf attr) == "list" then concatMapStringsSep " " (mapConfig key) attr else
80 if (builtins.typeOf attr) == "bool" then "--${key}=${boolToString attr}" else
81 if (builtins.typeOf attr) == "string" then "--${key}='${attr}'" else
82 "--${key}=${toString attr}");
83
84 configString = concatStringsSep " " (mapAttrsToList mapConfig allConfig);
85in
86{
87 options.services.oauth2_proxy = {
88 enable = mkEnableOption (lib.mdDoc "oauth2_proxy");
89
90 package = mkOption {
91 type = types.package;
92 default = pkgs.oauth2-proxy;
93 defaultText = literalExpression "pkgs.oauth2-proxy";
94 description = lib.mdDoc ''
95 The package that provides oauth2-proxy.
96 '';
97 };
98
99 ##############################################
100 # PROVIDER configuration
101 # Taken from: https://github.com/oauth2-proxy/oauth2-proxy/blob/master/providers/providers.go
102 provider = mkOption {
103 type = types.enum [
104 "adfs"
105 "azure"
106 "bitbucket"
107 "digitalocean"
108 "facebook"
109 "github"
110 "gitlab"
111 "google"
112 "keycloak"
113 "keycloak-oidc"
114 "linkedin"
115 "login.gov"
116 "nextcloud"
117 "oidc"
118 ];
119 default = "google";
120 description = lib.mdDoc ''
121 OAuth provider.
122 '';
123 };
124
125 approvalPrompt = mkOption {
126 type = types.enum ["force" "auto"];
127 default = "force";
128 description = lib.mdDoc ''
129 OAuth approval_prompt.
130 '';
131 };
132
133 clientID = mkOption {
134 type = types.nullOr types.str;
135 description = lib.mdDoc ''
136 The OAuth Client ID.
137 '';
138 example = "123456.apps.googleusercontent.com";
139 };
140
141 clientSecret = mkOption {
142 type = types.nullOr types.str;
143 description = lib.mdDoc ''
144 The OAuth Client Secret.
145 '';
146 };
147
148 skipAuthRegexes = mkOption {
149 type = types.listOf types.str;
150 default = [];
151 description = lib.mdDoc ''
152 Skip authentication for requests matching any of these regular
153 expressions.
154 '';
155 };
156
157 # XXX: Not clear whether these two options are mutually exclusive or not.
158 email = {
159 domains = mkOption {
160 type = types.listOf types.str;
161 default = [];
162 description = lib.mdDoc ''
163 Authenticate emails with the specified domains. Use
164 `*` to authenticate any email.
165 '';
166 };
167
168 addresses = mkOption {
169 type = types.nullOr types.lines;
170 default = null;
171 description = lib.mdDoc ''
172 Line-separated email addresses that are allowed to authenticate.
173 '';
174 };
175 };
176
177 loginURL = mkOption {
178 type = types.nullOr types.str;
179 default = null;
180 description = lib.mdDoc ''
181 Authentication endpoint.
182
183 You only need to set this if you are using a self-hosted provider (e.g.
184 Github Enterprise). If you're using a publicly hosted provider
185 (e.g github.com), then the default works.
186 '';
187 example = "https://provider.example.com/oauth/authorize";
188 };
189
190 redeemURL = mkOption {
191 type = types.nullOr types.str;
192 default = null;
193 description = lib.mdDoc ''
194 Token redemption endpoint.
195
196 You only need to set this if you are using a self-hosted provider (e.g.
197 Github Enterprise). If you're using a publicly hosted provider
198 (e.g github.com), then the default works.
199 '';
200 example = "https://provider.example.com/oauth/token";
201 };
202
203 validateURL = mkOption {
204 type = types.nullOr types.str;
205 default = null;
206 description = lib.mdDoc ''
207 Access token validation endpoint.
208
209 You only need to set this if you are using a self-hosted provider (e.g.
210 Github Enterprise). If you're using a publicly hosted provider
211 (e.g github.com), then the default works.
212 '';
213 example = "https://provider.example.com/user/emails";
214 };
215
216 redirectURL = mkOption {
217 # XXX: jml suspects this is always necessary, but the command-line
218 # doesn't require it so making it optional.
219 type = types.nullOr types.str;
220 default = null;
221 description = lib.mdDoc ''
222 The OAuth2 redirect URL.
223 '';
224 example = "https://internalapp.yourcompany.com/oauth2/callback";
225 };
226
227 azure = {
228 tenant = mkOption {
229 type = types.str;
230 default = "common";
231 description = lib.mdDoc ''
232 Go to a tenant-specific or common (tenant-independent) endpoint.
233 '';
234 };
235
236 resource = mkOption {
237 type = types.str;
238 description = lib.mdDoc ''
239 The resource that is protected.
240 '';
241 };
242 };
243
244 google = {
245 adminEmail = mkOption {
246 type = types.str;
247 description = lib.mdDoc ''
248 The Google Admin to impersonate for API calls.
249
250 Only users with access to the Admin APIs can access the Admin SDK
251 Directory API, thus the service account needs to impersonate one of
252 those users to access the Admin SDK Directory API.
253
254 See <https://developers.google.com/admin-sdk/directory/v1/guides/delegation#delegate_domain-wide_authority_to_your_service_account>.
255 '';
256 };
257
258 groups = mkOption {
259 type = types.listOf types.str;
260 default = [];
261 description = lib.mdDoc ''
262 Restrict logins to members of these Google groups.
263 '';
264 };
265
266 serviceAccountJSON = mkOption {
267 type = types.path;
268 description = lib.mdDoc ''
269 The path to the service account JSON credentials.
270 '';
271 };
272 };
273
274 github = {
275 org = mkOption {
276 type = types.nullOr types.str;
277 default = null;
278 description = lib.mdDoc ''
279 Restrict logins to members of this organisation.
280 '';
281 };
282
283 team = mkOption {
284 type = types.nullOr types.str;
285 default = null;
286 description = lib.mdDoc ''
287 Restrict logins to members of this team.
288 '';
289 };
290 };
291
292
293 ####################################################
294 # UPSTREAM Configuration
295 upstream = mkOption {
296 type = with types; coercedTo str (x: [x]) (listOf str);
297 default = [];
298 description = lib.mdDoc ''
299 The http url(s) of the upstream endpoint or `file://`
300 paths for static files. Routing is based on the path.
301 '';
302 };
303
304 passAccessToken = mkOption {
305 type = types.bool;
306 default = false;
307 description = lib.mdDoc ''
308 Pass OAuth access_token to upstream via X-Forwarded-Access-Token header.
309 '';
310 };
311
312 passBasicAuth = mkOption {
313 type = types.bool;
314 default = true;
315 description = lib.mdDoc ''
316 Pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream.
317 '';
318 };
319
320 basicAuthPassword = mkOption {
321 type = types.nullOr types.str;
322 default = null;
323 description = lib.mdDoc ''
324 The password to set when passing the HTTP Basic Auth header.
325 '';
326 };
327
328 passHostHeader = mkOption {
329 type = types.bool;
330 default = true;
331 description = lib.mdDoc ''
332 Pass the request Host Header to upstream.
333 '';
334 };
335
336 signatureKey = mkOption {
337 type = types.nullOr types.str;
338 default = null;
339 description = lib.mdDoc ''
340 GAP-Signature request signature key.
341 '';
342 example = "sha1:secret0";
343 };
344
345 cookie = {
346 domain = mkOption {
347 type = types.nullOr types.str;
348 default = null;
349 description = lib.mdDoc ''
350 Optional cookie domains to force cookies to (ie: `.yourcompany.com`).
351 The longest domain matching the request's host will be used (or the shortest
352 cookie domain if there is no match).
353 '';
354 example = ".yourcompany.com";
355 };
356
357 expire = mkOption {
358 type = types.str;
359 default = "168h0m0s";
360 description = lib.mdDoc ''
361 Expire timeframe for cookie.
362 '';
363 };
364
365 httpOnly = mkOption {
366 type = types.bool;
367 default = true;
368 description = lib.mdDoc ''
369 Set HttpOnly cookie flag.
370 '';
371 };
372
373 name = mkOption {
374 type = types.str;
375 default = "_oauth2_proxy";
376 description = lib.mdDoc ''
377 The name of the cookie that the oauth_proxy creates.
378 '';
379 };
380
381 refresh = mkOption {
382 # XXX: Unclear what the behavior is when this is not specified.
383 type = types.nullOr types.str;
384 default = null;
385 description = lib.mdDoc ''
386 Refresh the cookie after this duration; 0 to disable.
387 '';
388 example = "168h0m0s";
389 };
390
391 secret = mkOption {
392 type = types.nullOr types.str;
393 description = lib.mdDoc ''
394 The seed string for secure cookies.
395 '';
396 };
397
398 secure = mkOption {
399 type = types.bool;
400 default = true;
401 description = lib.mdDoc ''
402 Set secure (HTTPS) cookie flag.
403 '';
404 };
405 };
406
407 ####################################################
408 # OAUTH2 PROXY configuration
409
410 httpAddress = mkOption {
411 type = types.str;
412 default = "http://127.0.0.1:4180";
413 description = lib.mdDoc ''
414 HTTPS listening address. This module does not expose the port by
415 default. If you want this URL to be accessible to other machines, please
416 add the port to `networking.firewall.allowedTCPPorts`.
417 '';
418 };
419
420 htpasswd = {
421 file = mkOption {
422 type = types.nullOr types.path;
423 default = null;
424 description = lib.mdDoc ''
425 Additionally authenticate against a htpasswd file. Entries must be
426 created with `htpasswd -s` for SHA encryption.
427 '';
428 };
429
430 displayForm = mkOption {
431 type = types.bool;
432 default = true;
433 description = lib.mdDoc ''
434 Display username / password login form if an htpasswd file is provided.
435 '';
436 };
437 };
438
439 customTemplatesDir = mkOption {
440 type = types.nullOr types.path;
441 default = null;
442 description = lib.mdDoc ''
443 Path to custom HTML templates.
444 '';
445 };
446
447 reverseProxy = mkOption {
448 type = types.bool;
449 default = false;
450 description = lib.mdDoc ''
451 In case when running behind a reverse proxy, controls whether headers
452 like `X-Real-Ip` are accepted. Usage behind a reverse
453 proxy will require this flag to be set to avoid logging the reverse
454 proxy IP address.
455 '';
456 };
457
458 proxyPrefix = mkOption {
459 type = types.str;
460 default = "/oauth2";
461 description = lib.mdDoc ''
462 The url root path that this proxy should be nested under.
463 '';
464 };
465
466 tls = {
467 enable = mkOption {
468 type = types.bool;
469 default = false;
470 description = lib.mdDoc ''
471 Whether to serve over TLS.
472 '';
473 };
474
475 certificate = mkOption {
476 type = types.path;
477 description = lib.mdDoc ''
478 Path to certificate file.
479 '';
480 };
481
482 key = mkOption {
483 type = types.path;
484 description = lib.mdDoc ''
485 Path to private key file.
486 '';
487 };
488
489 httpsAddress = mkOption {
490 type = types.str;
491 default = ":443";
492 description = lib.mdDoc ''
493 `addr:port` to listen on for HTTPS clients.
494
495 Remember to add `port` to
496 `allowedTCPPorts` if you want other machines to be
497 able to connect to it.
498 '';
499 };
500 };
501
502 requestLogging = mkOption {
503 type = types.bool;
504 default = true;
505 description = lib.mdDoc ''
506 Log requests to stdout.
507 '';
508 };
509
510 ####################################################
511 # UNKNOWN
512
513 # XXX: Is this mandatory? Is it part of another group? Is it part of the provider specification?
514 scope = mkOption {
515 # XXX: jml suspects this is always necessary, but the command-line
516 # doesn't require it so making it optional.
517 type = types.nullOr types.str;
518 default = null;
519 description = lib.mdDoc ''
520 OAuth scope specification.
521 '';
522 };
523
524 profileURL = mkOption {
525 type = types.nullOr types.str;
526 default = null;
527 description = lib.mdDoc ''
528 Profile access endpoint.
529 '';
530 };
531
532 setXauthrequest = mkOption {
533 type = types.nullOr types.bool;
534 default = false;
535 description = lib.mdDoc ''
536 Set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode). Setting this to 'null' means using the upstream default (false).
537 '';
538 };
539
540 extraConfig = mkOption {
541 default = {};
542 type = types.attrsOf types.anything;
543 description = lib.mdDoc ''
544 Extra config to pass to oauth2-proxy.
545 '';
546 };
547
548 keyFile = mkOption {
549 type = types.nullOr types.path;
550 default = null;
551 description = lib.mdDoc ''
552 oauth2-proxy allows passing sensitive configuration via environment variables.
553 Make a file that contains lines like
554 OAUTH2_PROXY_CLIENT_SECRET=asdfasdfasdf.apps.googleuserscontent.com
555 and specify the path here.
556 '';
557 example = "/run/keys/oauth2_proxy";
558 };
559
560 };
561
562 config = mkIf cfg.enable {
563
564 services.oauth2_proxy = mkIf (cfg.keyFile != null) {
565 clientID = mkDefault null;
566 clientSecret = mkDefault null;
567 cookie.secret = mkDefault null;
568 };
569
570 users.users.oauth2_proxy = {
571 description = "OAuth2 Proxy";
572 isSystemUser = true;
573 group = "oauth2_proxy";
574 };
575
576 users.groups.oauth2_proxy = {};
577
578 systemd.services.oauth2_proxy = {
579 description = "OAuth2 Proxy";
580 path = [ cfg.package ];
581 wantedBy = [ "multi-user.target" ];
582 after = [ "network.target" ];
583
584 serviceConfig = {
585 User = "oauth2_proxy";
586 Restart = "always";
587 ExecStart = "${cfg.package}/bin/oauth2-proxy ${configString}";
588 EnvironmentFile = mkIf (cfg.keyFile != null) cfg.keyFile;
589 };
590 };
591
592 };
593}