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