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