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