[
  {
    "path": ".gitignore",
    "content": "oauth2_proxy\nvendor\ndist\n.godeps\n*.exe\n\n\n# Go.gitignore\n# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture specific extensions/prefixes\n*.[568vq]\n[568vq].out\n\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n\n_testmain.go\n\n# Editor swap/temp files\n.*.swp\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: go\ngo:\n  - 1.8.x\n  - 1.9.x\nscript:\n  - wget -O dep https://github.com/golang/dep/releases/download/v0.3.2/dep-linux-amd64\n  - chmod +x dep\n  - ./dep ensure\n  - ./test.sh\nsudo: false\nnotifications:\n  email: false\n"
  },
  {
    "path": "Gopkg.toml",
    "content": "\n# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md\n# for detailed Gopkg.toml documentation.\n#\n\n[[constraint]]\n  name = \"github.com/18F/hmacauth\"\n  version = \"~1.0.1\"\n\n[[constraint]]\n  name = \"github.com/BurntSushi/toml\"\n  version = \"~0.3.0\"\n\n[[constraint]]\n  name = \"github.com/bitly/go-simplejson\"\n  version = \"~0.5.0\"\n\n[[constraint]]\n  branch = \"v2\"\n  name = \"github.com/coreos/go-oidc\"\n\n[[constraint]]\n  branch = \"master\"\n  name = \"github.com/mreiferson/go-options\"\n\n[[constraint]]\n  name = \"github.com/stretchr/testify\"\n  version = \"~1.1.4\"\n\n[[constraint]]\n  branch = \"master\"\n  name = \"golang.org/x/oauth2\"\n\n[[constraint]]\n  branch = \"master\"\n  name = \"google.golang.org/api\"\n\n[[constraint]]\n  name = \"gopkg.in/fsnotify.v1\"\n  version = \"~1.2.0\"\n\n[[constraint]]\n  branch = \"master\"\n  name = \"golang.org/x/crypto\"\n"
  },
  {
    "path": "LICENSE",
    "content": "Permission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "oauth2_proxy\n=================\n\nA reverse proxy and static file server that provides authentication using Providers (Google, GitHub, and others)\nto validate accounts by email, domain or group.\n\n[![Build Status](https://secure.travis-ci.org/bitly/oauth2_proxy.svg?branch=master)](http://travis-ci.org/bitly/oauth2_proxy)\n\n\n![Sign In Page](https://cloud.githubusercontent.com/assets/45028/4970624/7feb7dd8-6886-11e4-93e0-c9904af44ea8.png)\n\n**NOTICE**: This project was officially archived by Bitly at the end of September 2018.\nBitly will no longer be accepting PRs or helping on issues.\nThere has been a [discussion](https://github.com/bitly/oauth2_proxy/issues/628)\nto find a new home for the project which has led to the following notable forks:\n\n- [pomerium](https://github.com/pomerium/pomerium) an identity-access proxy, inspired by BeyondCorp.\n- [buzzfeed/sso](https://github.com/buzzfeed/sso) a \"double OAuth2\" flow, where sso-auth is the OAuth2 provider for sso-proxy and Google is the OAuth2 provider for sso-auth.\n- [openshift/oauth_proxy](https://github.com/openshift/oauth-proxy) an openshift specific version of this project.\n- [pusher/oauth2_proxy](https://github.com/pusher/oauth2_proxy) official hard fork of this project.\n\nPlease submit all future PRs and issues to [pusher/oauth2_proxy](https://github.com/pusher/oauth2_proxy).\n\n## Architecture\n\n![OAuth2 Proxy Architecture](https://cloud.githubusercontent.com/assets/45028/8027702/bd040b7a-0d6a-11e5-85b9-f8d953d04f39.png)\n\n## Installation\n\n1. Download [Prebuilt Binary](https://github.com/bitly/oauth2_proxy/releases) (current release is `v2.2`) or build with `$ go get github.com/bitly/oauth2_proxy` which will put the binary in `$GOROOT/bin`\nPrebuilt binaries can be validated by extracting the file and verifying it against the `sha256sum.txt` checksum file provided for each release starting with version `v2.3`.\n```\nsha256sum -c sha256sum.txt 2>&1 | grep OK\noauth2_proxy-2.3.linux-amd64: OK\n```\n2. Select a Provider and Register an OAuth Application with a Provider\n3. Configure OAuth2 Proxy using config file, command line options, or environment variables\n4. Configure SSL or Deploy behind a SSL endpoint (example provided for Nginx)\n\n## OAuth Provider Configuration\n\nYou will need to register an OAuth application with a Provider (Google, GitHub or another provider), and configure it with Redirect URI(s) for the domain you intend to run `oauth2_proxy` on.\n\nValid providers are :\n\n* [Google](#google-auth-provider) *default*\n* [Azure](#azure-auth-provider)\n* [Facebook](#facebook-auth-provider)\n* [GitHub](#github-auth-provider)\n* [GitLab](#gitlab-auth-provider)\n* [LinkedIn](#linkedin-auth-provider)\n\nThe provider can be selected using the `provider` configuration value.\n\n### Google Auth Provider\n\nFor Google, the registration steps are:\n\n1. Create a new project: https://console.developers.google.com/project\n2. Choose the new project from the top right project dropdown (only if another project is selected)\n3. In the project Dashboard center pane, choose **\"API Manager\"**\n4. In the left Nav pane, choose **\"Credentials\"**\n5. In the center pane, choose **\"OAuth consent screen\"** tab. Fill in **\"Product name shown to users\"** and hit save.\n6. In the center pane, choose **\"Credentials\"** tab.\n   * Open the **\"New credentials\"** drop down\n   * Choose **\"OAuth client ID\"**\n   * Choose **\"Web application\"**\n   * Application name is freeform, choose something appropriate\n   * Authorized JavaScript origins is your domain ex: `https://internal.yourcompany.com`\n   * Authorized redirect URIs is the location of oauth2/callback ex: `https://internal.yourcompany.com/oauth2/callback`\n   * Choose **\"Create\"**\n4. Take note of the **Client ID** and **Client Secret**\n\nIt's recommended to refresh sessions on a short interval (1h) with `cookie-refresh` setting which validates that the account is still authorized.\n\n#### Restrict auth to specific Google groups on your domain. (optional)\n\n1. Create a service account: https://developers.google.com/identity/protocols/OAuth2ServiceAccount and make sure to download the json file.\n2. Make note of the Client ID for a future step.\n3. Under \"APIs & Auth\", choose APIs.\n4. Click on Admin SDK and then Enable API.\n5. Follow the steps on https://developers.google.com/admin-sdk/directory/v1/guides/delegation#delegate_domain-wide_authority_to_your_service_account and give the client id from step 2 the following oauth scopes:\n```\nhttps://www.googleapis.com/auth/admin.directory.group.readonly\nhttps://www.googleapis.com/auth/admin.directory.user.readonly\n```\n6. Follow the steps on https://support.google.com/a/answer/60757 to enable Admin API access.\n7. Create or choose an existing administrative email address on the Gmail domain to assign to the ```google-admin-email``` flag. This email will be impersonated by this client to make calls to the Admin SDK. See the note on the link from step 5 for the reason why.\n8. Create or choose an existing email group and set that email to the ```google-group``` flag. You can pass multiple instances of this flag with different groups\nand the user will be checked against all the provided groups.\n9. Lock down the permissions on the json file downloaded from step 1 so only oauth2_proxy is able to read the file and set the path to the file in the ```google-service-account-json``` flag.\n10. Restart oauth2_proxy.\n\nNote: The user is checked against the group members list on initial authentication and every time the token is refreshed ( about once an hour ).\n\n### Azure Auth Provider\n\n1. [Add an application](https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/) to your Azure Active Directory tenant.\n2. On the App properties page provide the correct Sign-On URL ie `https://internal.yourcompany.com/oauth2/callback`\n3. If applicable take note of your `TenantID` and provide it via the `--azure-tenant=<YOUR TENANT ID>` commandline option. Default the `common` tenant is used.\n\nThe Azure AD auth provider uses `openid` as it default scope. It uses `https://graph.windows.net` as a default protected resource. It call to `https://graph.windows.net/me` to get the email address of the user that logs in.\n\n\n### Facebook Auth Provider\n\n1. Create a new FB App from <https://developers.facebook.com/>\n2. Under FB Login, set your Valid OAuth redirect URIs to `https://internal.yourcompany.com/oauth2/callback`\n\n### GitHub Auth Provider\n\n1. Create a new project: https://github.com/settings/developers\n2. Under `Authorization callback URL` enter the correct url ie `https://internal.yourcompany.com/oauth2/callback`\n\nThe GitHub auth provider supports two additional parameters to restrict authentication to Organization or Team level access. Restricting by org and team is normally accompanied with `--email-domain=*`\n\n    -github-org=\"\": restrict logins to members of this organisation\n    -github-team=\"\": restrict logins to members of any of these teams (slug), separated by a comma\n\nIf you are using GitHub enterprise, make sure you set the following to the appropriate url:\n\n    -login-url=\"http(s)://<enterprise github host>/login/oauth/authorize\"\n    -redeem-url=\"http(s)://<enterprise github host>/login/oauth/access_token\"\n    -validate-url=\"http(s)://<enterprise github host>/api/v3\"\n\n### GitLab Auth Provider\n\nWhether you are using GitLab.com or self-hosting GitLab, follow [these steps to add an application](http://doc.gitlab.com/ce/integration/oauth_provider.html)\n\nIf you are using self-hosted GitLab, make sure you set the following to the appropriate URL:\n\n    -login-url=\"<your gitlab url>/oauth/authorize\"\n    -redeem-url=\"<your gitlab url>/oauth/token\"\n    -validate-url=\"<your gitlab url>/api/v4/user\"\n\n\n### LinkedIn Auth Provider\n\nFor LinkedIn, the registration steps are:\n\n1. Create a new project: https://www.linkedin.com/secure/developer\n2. In the OAuth User Agreement section:\n   * In default scope, select r_basicprofile and r_emailaddress.\n   * In \"OAuth 2.0 Redirect URLs\", enter `https://internal.yourcompany.com/oauth2/callback`\n3. Fill in the remaining required fields and Save.\n4. Take note of the **Consumer Key / API Key** and **Consumer Secret / Secret Key**\n\n### Microsoft Azure AD Provider\n\nFor adding an application to the Microsoft Azure AD follow [these steps to add an application](https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/).\n\nTake note of your `TenantId` if applicable for your situation. The `TenantId` can be used to override the default `common` authorization server with a tenant specific server.\n\n### OpenID Connect Provider\n\nOpenID Connect is a spec for OAUTH 2.0 + identity that is implemented by many major providers and several open source projects. This provider was originally built against CoreOS Dex and we will use it as an example.\n\n1. Launch a Dex instance using the [getting started guide](https://github.com/coreos/dex/blob/master/Documentation/getting-started.md).\n2. Setup oauth2_proxy with the correct provider and using the default ports and callbacks.\n3. Login with the fixture use in the dex guide and run the oauth2_proxy with the following args:\n\n    -provider oidc\n    -client-id oauth2_proxy\n    -client-secret proxy\n    -redirect-url http://127.0.0.1:4180/oauth2/callback\n    -oidc-issuer-url http://127.0.0.1:5556\n    -cookie-secure=false\n    -email-domain example.com\n\n## Email Authentication\n\nTo authorize by email domain use `--email-domain=yourcompany.com`. To authorize individual email addresses use `--authenticated-emails-file=/path/to/file` with one email per line. To authorize all email addresses use `--email-domain=*`.\n\n## Configuration\n\n`oauth2_proxy` can be configured via [config file](#config-file), [command line options](#command-line-options) or [environment variables](#environment-variables).\n\nTo generate a strong cookie secret use `python -c 'import os,base64; print base64.urlsafe_b64encode(os.urandom(16))'`\n\n### Config File\n\nAn example [oauth2_proxy.cfg](contrib/oauth2_proxy.cfg.example) config file is in the contrib directory. It can be used by specifying `-config=/etc/oauth2_proxy.cfg`\n\n### Command Line Options\n\n```\nUsage of oauth2_proxy:\n  -approval-prompt string: OAuth approval_prompt (default \"force\")\n  -authenticated-emails-file string: authenticate against emails via file (one per line)\n  -azure-tenant string: go to a tenant-specific or common (tenant-independent) endpoint. (default \"common\")\n  -basic-auth-password string: the password to set when passing the HTTP Basic Auth header\n  -client-id string: the OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"\n  -client-secret string: the OAuth Client Secret\n  -config string: path to config file\n  -cookie-domain string: an optional cookie domain to force cookies to (ie: .yourcompany.com)\n  -cookie-expire duration: expire timeframe for cookie (default 168h0m0s)\n  -cookie-httponly: set HttpOnly cookie flag (default true)\n  -cookie-name string: the name of the cookie that the oauth_proxy creates (default \"_oauth2_proxy\")\n  -cookie-refresh duration: refresh the cookie after this duration; 0 to disable\n  -cookie-secret string: the seed string for secure cookies (optionally base64 encoded)\n  -cookie-secure: set secure (HTTPS) cookie flag (default true)\n  -custom-templates-dir string: path to custom html templates\n  -display-htpasswd-form: display username / password login form if an htpasswd file is provided (default true)\n  -email-domain value: authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email\n  -footer string: custom footer string. Use \"-\" to disable default footer.\n  -github-org string: restrict logins to members of this organisation\n  -github-team string: restrict logins to members of any of these teams (slug), separated by a comma\n  -google-admin-email string: the google admin to impersonate for api calls\n  -google-group value: restrict logins to members of this google group (may be given multiple times).\n  -google-service-account-json string: the path to the service account json credentials\n  -htpasswd-file string: additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption\n  -http-address string: [http://]<addr>:<port> or unix://<path> to listen on for HTTP clients (default \"127.0.0.1:4180\")\n  -https-address string: <addr>:<port> to listen on for HTTPS clients (default \":443\")\n  -login-url string: Authentication endpoint\n  -pass-access-token: pass OAuth access_token to upstream via X-Forwarded-Access-Token header\n  -pass-basic-auth: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream (default true)\n  -pass-host-header: pass the request Host Header to upstream (default true)\n  -pass-user-headers: pass X-Forwarded-User and X-Forwarded-Email information to upstream (default true)\n  -profile-url string: Profile access endpoint\n  -provider string: OAuth provider (default \"google\")\n  -proxy-prefix string: the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in) (default \"/oauth2\")\n  -redeem-url string: Token redemption endpoint\n  -redirect-url string: the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"\n  -request-logging: Log requests to stdout (default true)\n  -request-logging-format: Template for request log lines (see \"Logging Format\" paragraph below)\n  -resource string: The resource that is protected (Azure AD only)\n  -scope string: OAuth scope specification\n  -set-xauthrequest: set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)\n  -signature-key string: GAP-Signature request signature key (algorithm:secretkey)\n  -skip-auth-preflight: will skip authentication for OPTIONS requests\n  -skip-auth-regex value: bypass authentication for requests path's that match (may be given multiple times)\n  -skip-provider-button: will skip sign-in-page to directly reach the next step: oauth/start\n  -ssl-insecure-skip-verify: skip validation of certificates presented when using HTTPS\n  -tls-cert string: path to certificate file\n  -tls-key string: path to private key file\n  -upstream value: the http url(s) of the upstream endpoint or file:// paths for static files. Routing is based on the path\n  -validate-url string: Access token validation endpoint\n  -version: print version string\n```\n\nSee below for provider specific options\n\n### Upstreams Configuration\n\n`oauth2_proxy` supports having multiple upstreams, and has the option to pass requests on to HTTP(S) servers or serve static files from the file system. HTTP and HTTPS upstreams are configured by providing a URL such as `http://127.0.0.1:8080/` for the upstream parameter, that will forward all authenticated requests to be forwarded to the upstream server. If you instead provide `http://127.0.0.1:8080/some/path/` then it will only be requests that start with `/some/path/` which are forwarded to the upstream.\n\nStatic file paths are configured as a file:// URL. `file:///var/www/static/` will serve the files from that directory at `http://[oauth2_proxy url]/var/www/static/`, which may not be what you want. You can provide the path to where the files should be available by adding a fragment to the configured URL. The value of the fragment will then be used to specify which path the files are available at. `file:///var/www/static/#/static/` will ie. make `/var/www/static/` available at `http://[oauth2_proxy url]/static/`.\n\nMultiple upstreams can either be configured by supplying a comma separated list to the `-upstream` parameter, supplying the parameter multiple times or provinding a list in the [config file](#config-file). When multiple upstreams are used routing to them will be based on the path they are set up with.\n\n### Environment variables\n\nThe following environment variables can be used in place of the corresponding command-line arguments:\n\n- `OAUTH2_PROXY_CLIENT_ID`\n- `OAUTH2_PROXY_CLIENT_SECRET`\n- `OAUTH2_PROXY_COOKIE_NAME`\n- `OAUTH2_PROXY_COOKIE_SECRET`\n- `OAUTH2_PROXY_COOKIE_DOMAIN`\n- `OAUTH2_PROXY_COOKIE_EXPIRE`\n- `OAUTH2_PROXY_COOKIE_REFRESH`\n- `OAUTH2_PROXY_SIGNATURE_KEY`\n\n## SSL Configuration\n\nThere are two recommended configurations.\n\n1) Configure SSL Termination with OAuth2 Proxy by providing a `--tls-cert=/path/to/cert.pem` and `--tls-key=/path/to/cert.key`.\n\nThe command line to run `oauth2_proxy` in this configuration would look like this:\n\n```bash\n./oauth2_proxy \\\n   --email-domain=\"yourcompany.com\"  \\\n   --upstream=http://127.0.0.1:8080/ \\\n   --tls-cert=/path/to/cert.pem \\\n   --tls-key=/path/to/cert.key \\\n   --cookie-secret=... \\\n   --cookie-secure=true \\\n   --provider=... \\\n   --client-id=... \\\n   --client-secret=...\n```\n\n\n2) Configure SSL Termination with [Nginx](http://nginx.org/) (example config below), Amazon ELB, Google Cloud Platform Load Balancing, or ....\n\nBecause `oauth2_proxy` listens on `127.0.0.1:4180` by default, to listen on all interfaces (needed when using an\nexternal load balancer like Amazon ELB or Google Platform Load Balancing) use `--http-address=\"0.0.0.0:4180\"` or\n`--http-address=\"http://:4180\"`.\n\nNginx will listen on port `443` and handle SSL connections while proxying to `oauth2_proxy` on port `4180`.\n`oauth2_proxy` will then authenticate requests for an upstream application. The external endpoint for this example\nwould be `https://internal.yourcompany.com/`.\n\nAn example Nginx config follows. Note the use of `Strict-Transport-Security` header to pin requests to SSL\nvia [HSTS](http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security):\n\n```\nserver {\n    listen 443 default ssl;\n    server_name internal.yourcompany.com;\n    ssl_certificate /path/to/cert.pem;\n    ssl_certificate_key /path/to/cert.key;\n    add_header Strict-Transport-Security max-age=2592000;\n\n    location / {\n        proxy_pass http://127.0.0.1:4180;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Scheme $scheme;\n        proxy_connect_timeout 1;\n        proxy_send_timeout 30;\n        proxy_read_timeout 30;\n    }\n}\n```\n\nThe command line to run `oauth2_proxy` in this configuration would look like this:\n\n```bash\n./oauth2_proxy \\\n   --email-domain=\"yourcompany.com\"  \\\n   --upstream=http://127.0.0.1:8080/ \\\n   --cookie-secret=... \\\n   --cookie-secure=true \\\n   --provider=... \\\n   --client-id=... \\\n   --client-secret=...\n```\n\n## Endpoint Documentation\n\nOAuth2 Proxy responds directly to the following endpoints. All other endpoints will be proxied upstream when authenticated. The `/oauth2` prefix can be changed with the `--proxy-prefix` config variable.\n\n* /robots.txt - returns a 200 OK response that disallows all User-agents from all paths; see [robotstxt.org](http://www.robotstxt.org/) for more info\n* /ping - returns an 200 OK response\n* /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies)\n* /oauth2/start - a URL that will redirect to start the OAuth cycle\n* /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url.\n* /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](#nginx-auth-request)\n\n## Request signatures\n\nIf `signature_key` is defined, proxied requests will be signed with the\n`GAP-Signature` header, which is a [Hash-based Message Authentication Code\n(HMAC)](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code)\nof selected request information and the request body [see `SIGNATURE_HEADERS`\nin `oauthproxy.go`](./oauthproxy.go).\n\n`signature_key` must be of the form `algorithm:secretkey`, (ie: `signature_key = \"sha1:secret0\"`)\n\nFor more information about HMAC request signature validation, read the\nfollowing:\n\n* [Amazon Web Services: Signing and Authenticating REST\n  Requests](https://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html)\n* [rc3.org: Using HMAC to authenticate Web service\n  requests](http://rc3.org/2011/12/02/using-hmac-to-authenticate-web-service-requests/)\n\n## Logging Format\n\nBy default, OAuth2 Proxy logs requests to stdout in a format similar to Apache Combined Log.\n\n```\n<REMOTE_ADDRESS> - <user@domain.com> [19/Mar/2015:17:20:19 -0400] <HOST_HEADER> GET <UPSTREAM_HOST> \"/path/\" HTTP/1.1 \"<USER_AGENT>\" <RESPONSE_CODE> <RESPONSE_BYTES> <REQUEST_DURATION>\n```\n\nIf you require a different format than that, you can configure it with the `-request-logging-format` flag.\nThe default format is configured as follows:\n\n```\n{{.Client}} - {{.Username}} [{{.Timestamp}}] {{.Host}} {{.RequestMethod}} {{.Upstream}} {{.RequestURI}} {{.Protocol}} {{.UserAgent}} {{.StatusCode}} {{.ResponseSize}} {{.RequestDuration}}\n```\n\n[See `logMessageData` in `logging_handler.go`](./logging_handler.go) for all available variables.\n\n## Adding a new Provider\n\nFollow the examples in the [`providers` package](providers/) to define a new\n`Provider` instance. Add a new `case` to\n[`providers.New()`](providers/providers.go) to allow `oauth2_proxy` to use the\nnew `Provider`.\n\n## <a name=\"nginx-auth-request\"></a>Configuring for use with the Nginx `auth_request` directive\n\nThe [Nginx `auth_request` directive](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) allows Nginx to authenticate requests via the oauth2_proxy's `/auth` endpoint, which only returns a 202 Accepted response or a 401 Unauthorized response without proxying the request through. For example:\n\n```nginx\nserver {\n  listen 443 ssl;\n  server_name ...;\n  include ssl/ssl.conf;\n\n  location /oauth2/ {\n    proxy_pass       http://127.0.0.1:4180;\n    proxy_set_header Host                    $host;\n    proxy_set_header X-Real-IP               $remote_addr;\n    proxy_set_header X-Scheme                $scheme;\n    proxy_set_header X-Auth-Request-Redirect $request_uri;\n  }\n  location = /oauth2/auth {\n    proxy_pass       http://127.0.0.1:4180;\n    proxy_set_header Host             $host;\n    proxy_set_header X-Real-IP        $remote_addr;\n    proxy_set_header X-Scheme         $scheme;\n    # nginx auth_request includes headers but not body\n    proxy_set_header Content-Length   \"\";\n    proxy_pass_request_body           off;\n  }\n\n  location / {\n    auth_request /oauth2/auth;\n    error_page 401 = /oauth2/sign_in;\n\n    # pass information via X-User and X-Email headers to backend,\n    # requires running with --set-xauthrequest flag\n    auth_request_set $user   $upstream_http_x_auth_request_user;\n    auth_request_set $email  $upstream_http_x_auth_request_email;\n    proxy_set_header X-User  $user;\n    proxy_set_header X-Email $email;\n\n    # if you enabled --cookie-refresh, this is needed for it to work with auth_request\n    auth_request_set $auth_cookie $upstream_http_set_cookie;\n    add_header Set-Cookie $auth_cookie;\n\n    proxy_pass http://backend/;\n    # or \"root /path/to/site;\" or \"fastcgi_pass ...\" etc\n  }\n}\n```\n"
  },
  {
    "path": "api/api.go",
    "content": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/bitly/go-simplejson\"\n)\n\nfunc Request(req *http.Request) (*simplejson.Json, error) {\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tlog.Printf(\"%s %s %s\", req.Method, req.URL, err)\n\t\treturn nil, err\n\t}\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tresp.Body.Close()\n\tlog.Printf(\"%d %s %s %s\", resp.StatusCode, req.Method, req.URL, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.StatusCode != 200 {\n\t\treturn nil, fmt.Errorf(\"got %d %s\", resp.StatusCode, body)\n\t}\n\tdata, err := simplejson.NewJson(body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn data, nil\n}\n\nfunc RequestJson(req *http.Request, v interface{}) error {\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tlog.Printf(\"%s %s %s\", req.Method, req.URL, err)\n\t\treturn err\n\t}\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tresp.Body.Close()\n\tlog.Printf(\"%d %s %s %s\", resp.StatusCode, req.Method, req.URL, body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"got %d %s\", resp.StatusCode, body)\n\t}\n\treturn json.Unmarshal(body, v)\n}\n\nfunc RequestUnparsedResponse(url string, header http.Header) (resp *http.Response, err error) {\n\treq, err := http.NewRequest(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header = header\n\n\treturn http.DefaultClient.Do(req)\n}\n"
  },
  {
    "path": "api/api_test.go",
    "content": "package api\n\nimport (\n\t\"github.com/bitly/go-simplejson\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc testBackend(response_code int, payload string) *httptest.Server {\n\treturn httptest.NewServer(http.HandlerFunc(\n\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.WriteHeader(response_code)\n\t\t\tw.Write([]byte(payload))\n\t\t}))\n}\n\nfunc TestRequest(t *testing.T) {\n\tbackend := testBackend(200, \"{\\\"foo\\\": \\\"bar\\\"}\")\n\tdefer backend.Close()\n\n\treq, _ := http.NewRequest(\"GET\", backend.URL, nil)\n\tresponse, err := Request(req)\n\tassert.Equal(t, nil, err)\n\tresult, err := response.Get(\"foo\").String()\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, \"bar\", result)\n}\n\nfunc TestRequestFailure(t *testing.T) {\n\t// Create a backend to generate a test URL, then close it to cause a\n\t// connection error.\n\tbackend := testBackend(200, \"{\\\"foo\\\": \\\"bar\\\"}\")\n\tbackend.Close()\n\n\treq, err := http.NewRequest(\"GET\", backend.URL, nil)\n\tassert.Equal(t, nil, err)\n\tresp, err := Request(req)\n\tassert.Equal(t, (*simplejson.Json)(nil), resp)\n\tassert.NotEqual(t, nil, err)\n\tif !strings.Contains(err.Error(), \"refused\") {\n\t\tt.Error(\"expected error when a connection fails: \", err)\n\t}\n}\n\nfunc TestHttpErrorCode(t *testing.T) {\n\tbackend := testBackend(404, \"{\\\"foo\\\": \\\"bar\\\"}\")\n\tdefer backend.Close()\n\n\treq, err := http.NewRequest(\"GET\", backend.URL, nil)\n\tassert.Equal(t, nil, err)\n\tresp, err := Request(req)\n\tassert.Equal(t, (*simplejson.Json)(nil), resp)\n\tassert.NotEqual(t, nil, err)\n}\n\nfunc TestJsonParsingError(t *testing.T) {\n\tbackend := testBackend(200, \"not well-formed JSON\")\n\tdefer backend.Close()\n\n\treq, err := http.NewRequest(\"GET\", backend.URL, nil)\n\tassert.Equal(t, nil, err)\n\tresp, err := Request(req)\n\tassert.Equal(t, (*simplejson.Json)(nil), resp)\n\tassert.NotEqual(t, nil, err)\n}\n\n// Parsing a URL practically never fails, so we won't cover that test case.\nfunc TestRequestUnparsedResponseUsingAccessTokenParameter(t *testing.T) {\n\tbackend := httptest.NewServer(http.HandlerFunc(\n\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\ttoken := r.FormValue(\"access_token\")\n\t\t\tif r.URL.Path == \"/\" && token == \"my_token\" {\n\t\t\t\tw.WriteHeader(200)\n\t\t\t\tw.Write([]byte(\"some payload\"))\n\t\t\t} else {\n\t\t\t\tw.WriteHeader(403)\n\t\t\t}\n\t\t}))\n\tdefer backend.Close()\n\n\tresponse, err := RequestUnparsedResponse(\n\t\tbackend.URL+\"?access_token=my_token\", nil)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 200, response.StatusCode)\n\tbody, err := ioutil.ReadAll(response.Body)\n\tassert.Equal(t, nil, err)\n\tresponse.Body.Close()\n\tassert.Equal(t, \"some payload\", string(body))\n}\n\nfunc TestRequestUnparsedResponseUsingAccessTokenParameterFailedResponse(t *testing.T) {\n\tbackend := testBackend(200, \"some payload\")\n\t// Close the backend now to force a request failure.\n\tbackend.Close()\n\n\tresponse, err := RequestUnparsedResponse(\n\t\tbackend.URL+\"?access_token=my_token\", nil)\n\tassert.NotEqual(t, nil, err)\n\tassert.Equal(t, (*http.Response)(nil), response)\n}\n\nfunc TestRequestUnparsedResponseUsingHeaders(t *testing.T) {\n\tbackend := httptest.NewServer(http.HandlerFunc(\n\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.URL.Path == \"/\" && r.Header[\"Auth\"][0] == \"my_token\" {\n\t\t\t\tw.WriteHeader(200)\n\t\t\t\tw.Write([]byte(\"some payload\"))\n\t\t\t} else {\n\t\t\t\tw.WriteHeader(403)\n\t\t\t}\n\t\t}))\n\tdefer backend.Close()\n\n\theaders := make(http.Header)\n\theaders.Set(\"Auth\", \"my_token\")\n\tresponse, err := RequestUnparsedResponse(backend.URL, headers)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 200, response.StatusCode)\n\tbody, err := ioutil.ReadAll(response.Body)\n\tassert.Equal(t, nil, err)\n\tresponse.Body.Close()\n\tassert.Equal(t, \"some payload\", string(body))\n}\n"
  },
  {
    "path": "contrib/oauth2_proxy.cfg.example",
    "content": "## OAuth2 Proxy Config File\n## https://github.com/bitly/oauth2_proxy\n\n## <addr>:<port> to listen on for HTTP/HTTPS clients\n# http_address = \"127.0.0.1:4180\"\n# https_address = \":443\"\n\n## TLS Settings\n# tls_cert_file = \"\"\n# tls_key_file = \"\"\n\n## the OAuth Redirect URL.\n# defaults to the \"https://\" + requested host header + \"/oauth2/callback\"\n# redirect_url = \"https://internalapp.yourcompany.com/oauth2/callback\"\n\n## the http url(s) of the upstream endpoint. If multiple, routing is based on path\n# upstreams = [\n#     \"http://127.0.0.1:8080/\"\n# ]\n\n## Log requests to stdout\n# request_logging = true\n\n## pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream\n# pass_basic_auth = true\n# pass_user_headers = true\n## pass the request Host Header to upstream\n## when disabled the upstream Host is used as the Host Header\n# pass_host_header = true \n\n## Email Domains to allow authentication for (this authorizes any email on this domain)\n## for more granular authorization use `authenticated_emails_file`\n## To authorize any email addresses use \"*\"\n# email_domains = [\n#     \"yourcompany.com\"\n# ]\n\n## The OAuth Client ID, Secret\n# client_id = \"123456.apps.googleusercontent.com\"\n# client_secret = \"\"\n\n## Pass OAuth Access token to upstream via \"X-Forwarded-Access-Token\"\n# pass_access_token = false\n\n## Authenticated Email Addresses File (one email per line)\n# authenticated_emails_file = \"\"\n\n## Htpasswd File (optional)\n## Additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption\n## enabling exposes a username/login signin form\n# htpasswd_file = \"\"\n\n## Templates\n## optional directory with custom sign_in.html and error.html\n# custom_templates_dir = \"\"\n\n## skip SSL checking for HTTPS requests\n# ssl_insecure_skip_verify = false\n\n\n## Cookie Settings\n## Name     - the cookie name\n## Secret   - the seed string for secure cookies; should be 16, 24, or 32 bytes\n##            for use with an AES cipher when cookie_refresh or pass_access_token\n##            is set\n## Domain   - (optional) cookie domain to force cookies to (ie: .yourcompany.com)\n## Expire   - (duration) expire timeframe for cookie\n## Refresh  - (duration) refresh the cookie when duration has elapsed after cookie was initially set.\n##            Should be less than cookie_expire; set to 0 to disable.\n##            On refresh, OAuth token is re-validated. \n##            (ie: 1h means tokens are refreshed on request 1hr+ after it was set)\n## Secure   - secure cookies are only sent by the browser of a HTTPS connection (recommended)\n## HttpOnly - httponly cookies are not readable by javascript (recommended)\n# cookie_name = \"_oauth2_proxy\"\n# cookie_secret = \"\"\n# cookie_domain = \"\"\n# cookie_expire = \"168h\"\n# cookie_refresh = \"\"\n# cookie_secure = true\n# cookie_httponly = true\n"
  },
  {
    "path": "contrib/oauth2_proxy.service.example",
    "content": "# Systemd service file for oauth2_proxy daemon\n#\n# Date: Feb 9, 2016\n# Author: Srdjan Grubor <sgnn7@sgnn7.org>\n\n[Unit]\nDescription=oauth2_proxy daemon service\nAfter=syslog.target network.target\n\n[Service]\n# www-data group and user need to be created before using these lines\nUser=www-data\nGroup=www-data\n\nExecStart=/usr/local/bin/oauth2_proxy -config=/etc/oauth2_proxy.cfg\nExecReload=/bin/kill -HUP $MAINPID\n\nKillMode=process\nRestart=always\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "cookie/cookies.go",
    "content": "package cookie\n\nimport (\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/hmac\"\n\t\"crypto/rand\"\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// cookies are stored in a 3 part (value + timestamp + signature) to enforce that the values are as originally set.\n// additionally, the 'value' is encrypted so it's opaque to the browser\n\n// Validate ensures a cookie is properly signed\nfunc Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value string, t time.Time, ok bool) {\n\t// value, timestamp, sig\n\tparts := strings.Split(cookie.Value, \"|\")\n\tif len(parts) != 3 {\n\t\treturn\n\t}\n\tsig := cookieSignature(seed, cookie.Name, parts[0], parts[1])\n\tif checkHmac(parts[2], sig) {\n\t\tts, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\t// The expiration timestamp set when the cookie was created\n\t\t// isn't sent back by the browser. Hence, we check whether the\n\t\t// creation timestamp stored in the cookie falls within the\n\t\t// window defined by (Now()-expiration, Now()].\n\t\tt = time.Unix(int64(ts), 0)\n\t\tif t.After(time.Now().Add(expiration*-1)) && t.Before(time.Now().Add(time.Minute*5)) {\n\t\t\t// it's a valid cookie. now get the contents\n\t\t\trawValue, err := base64.URLEncoding.DecodeString(parts[0])\n\t\t\tif err == nil {\n\t\t\t\tvalue = string(rawValue)\n\t\t\t\tok = true\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\n// SignedValue returns a cookie that is signed and can later be checked with Validate\nfunc SignedValue(seed string, key string, value string, now time.Time) string {\n\tencodedValue := base64.URLEncoding.EncodeToString([]byte(value))\n\ttimeStr := fmt.Sprintf(\"%d\", now.Unix())\n\tsig := cookieSignature(seed, key, encodedValue, timeStr)\n\tcookieVal := fmt.Sprintf(\"%s|%s|%s\", encodedValue, timeStr, sig)\n\treturn cookieVal\n}\n\nfunc cookieSignature(args ...string) string {\n\th := hmac.New(sha1.New, []byte(args[0]))\n\tfor _, arg := range args[1:] {\n\t\th.Write([]byte(arg))\n\t}\n\tvar b []byte\n\tb = h.Sum(b)\n\treturn base64.URLEncoding.EncodeToString(b)\n}\n\nfunc checkHmac(input, expected string) bool {\n\tinputMAC, err1 := base64.URLEncoding.DecodeString(input)\n\tif err1 == nil {\n\t\texpectedMAC, err2 := base64.URLEncoding.DecodeString(expected)\n\t\tif err2 == nil {\n\t\t\treturn hmac.Equal(inputMAC, expectedMAC)\n\t\t}\n\t}\n\treturn false\n}\n\n// Cipher provides methods to encrypt and decrypt cookie values\ntype Cipher struct {\n\tcipher.Block\n}\n\n// NewCipher returns a new aes Cipher for encrypting cookie values\nfunc NewCipher(secret []byte) (*Cipher, error) {\n\tc, err := aes.NewCipher(secret)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Cipher{Block: c}, err\n}\n\n// Encrypt a value for use in a cookie\nfunc (c *Cipher) Encrypt(value string) (string, error) {\n\tciphertext := make([]byte, aes.BlockSize+len(value))\n\tiv := ciphertext[:aes.BlockSize]\n\tif _, err := io.ReadFull(rand.Reader, iv); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create initialization vector %s\", err)\n\t}\n\n\tstream := cipher.NewCFBEncrypter(c.Block, iv)\n\tstream.XORKeyStream(ciphertext[aes.BlockSize:], []byte(value))\n\treturn base64.StdEncoding.EncodeToString(ciphertext), nil\n}\n\n// Decrypt a value from a cookie to it's original string\nfunc (c *Cipher) Decrypt(s string) (string, error) {\n\tencrypted, err := base64.StdEncoding.DecodeString(s)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to decrypt cookie value %s\", err)\n\t}\n\n\tif len(encrypted) < aes.BlockSize {\n\t\treturn \"\", fmt.Errorf(\"encrypted cookie value should be \"+\n\t\t\t\"at least %d bytes, but is only %d bytes\",\n\t\t\taes.BlockSize, len(encrypted))\n\t}\n\n\tiv := encrypted[:aes.BlockSize]\n\tencrypted = encrypted[aes.BlockSize:]\n\tstream := cipher.NewCFBDecrypter(c.Block, iv)\n\tstream.XORKeyStream(encrypted, encrypted)\n\n\treturn string(encrypted), nil\n}\n"
  },
  {
    "path": "cookie/cookies_test.go",
    "content": "package cookie\n\nimport (\n\t\"encoding/base64\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestEncodeAndDecodeAccessToken(t *testing.T) {\n\tconst secret = \"0123456789abcdefghijklmnopqrstuv\"\n\tconst token = \"my access token\"\n\tc, err := NewCipher([]byte(secret))\n\tassert.Equal(t, nil, err)\n\n\tencoded, err := c.Encrypt(token)\n\tassert.Equal(t, nil, err)\n\n\tdecoded, err := c.Decrypt(encoded)\n\tassert.Equal(t, nil, err)\n\n\tassert.NotEqual(t, token, encoded)\n\tassert.Equal(t, token, decoded)\n}\n\nfunc TestEncodeAndDecodeAccessTokenB64(t *testing.T) {\n\tconst secret_b64 = \"A3Xbr6fu6Al0HkgrP1ztjb-mYiwmxgNPP-XbNsz1WBk=\"\n\tconst token = \"my access token\"\n\n\tsecret, err := base64.URLEncoding.DecodeString(secret_b64)\n\tc, err := NewCipher([]byte(secret))\n\tassert.Equal(t, nil, err)\n\n\tencoded, err := c.Encrypt(token)\n\tassert.Equal(t, nil, err)\n\n\tdecoded, err := c.Decrypt(encoded)\n\tassert.Equal(t, nil, err)\n\n\tassert.NotEqual(t, token, encoded)\n\tassert.Equal(t, token, decoded)\n}\n"
  },
  {
    "path": "cookie/nonce.go",
    "content": "package cookie\n\nimport (\n\t\"crypto/rand\"\n\t\"fmt\"\n)\n\nfunc Nonce() (nonce string, err error) {\n\tb := make([]byte, 16)\n\t_, err = rand.Read(b)\n\tif err != nil {\n\t\treturn\n\t}\n\tnonce = fmt.Sprintf(\"%x\", b)\n\treturn\n}\n"
  },
  {
    "path": "dist.sh",
    "content": "#!/bin/bash\n# build binary distributions for linux/amd64 and darwin/amd64\nset -e\n\nDIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\necho \"working dir $DIR\"\nmkdir -p $DIR/dist\ndep ensure || exit 1\n\nos=$(go env GOOS)\narch=$(go env GOARCH)\nversion=$(cat $DIR/version.go | grep \"const VERSION\" | awk '{print $NF}' | sed 's/\"//g')\ngoversion=$(go version | awk '{print $3}')\nsha256sum=()\n\necho \"... running tests\"\n./test.sh\n\nfor os in windows linux darwin; do\n    echo \"... building v$version for $os/$arch\"\n    EXT=\n    if [ $os = windows ]; then\n        EXT=\".exe\"\n    fi\n    BUILD=$(mktemp -d ${TMPDIR:-/tmp}/oauth2_proxy.XXXXXX)\n    TARGET=\"oauth2_proxy-$version.$os-$arch.$goversion\"\n    FILENAME=\"oauth2_proxy-$version.$os-$arch$EXT\"\n    GOOS=$os GOARCH=$arch CGO_ENABLED=0 \\\n        go build -ldflags=\"-s -w\" -o $BUILD/$TARGET/$FILENAME || exit 1\n    pushd $BUILD/$TARGET\n    sha256sum+=(\"$(shasum -a 256 $FILENAME || exit 1)\")\n    cd .. && tar czvf $TARGET.tar.gz $TARGET\n    mv $TARGET.tar.gz $DIR/dist\n    popd\ndone\n\nchecksum_file=\"sha256sum.txt\"\ncd $DIR/dist\nif [ -f $checksum_file ]; then\n    rm $checksum_file\nfi\ntouch $checksum_file\nfor checksum in \"${sha256sum[@]}\"; do\n    echo \"$checksum\" >> $checksum_file\ndone\n"
  },
  {
    "path": "env_options.go",
    "content": "package main\n\nimport (\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n)\n\ntype EnvOptions map[string]interface{}\n\nfunc (cfg EnvOptions) LoadEnvForStruct(options interface{}) {\n\tval := reflect.ValueOf(options).Elem()\n\ttyp := val.Type()\n\tfor i := 0; i < typ.NumField(); i++ {\n\t\t// pull out the struct tags:\n\t\t//    flag - the name of the command line flag\n\t\t//    deprecated - (optional) the name of the deprecated command line flag\n\t\t//    cfg - (optional, defaults to underscored flag) the name of the config file option\n\t\tfield := typ.Field(i)\n\t\tflagName := field.Tag.Get(\"flag\")\n\t\tenvName := field.Tag.Get(\"env\")\n\t\tcfgName := field.Tag.Get(\"cfg\")\n\t\tif cfgName == \"\" && flagName != \"\" {\n\t\t\tcfgName = strings.Replace(flagName, \"-\", \"_\", -1)\n\t\t}\n\t\tif envName == \"\" || cfgName == \"\" {\n\t\t\t// resolvable fields must have the `env` and `cfg` struct tag\n\t\t\tcontinue\n\t\t}\n\t\tv := os.Getenv(envName)\n\t\tif v != \"\" {\n\t\t\tcfg[cfgName] = v\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "env_options_test.go",
    "content": "package main\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype envTest struct {\n\ttestField string `cfg:\"target_field\" env:\"TEST_ENV_FIELD\"`\n}\n\nfunc TestLoadEnvForStruct(t *testing.T) {\n\n\tcfg := make(EnvOptions)\n\tcfg.LoadEnvForStruct(&envTest{})\n\n\t_, ok := cfg[\"target_field\"]\n\tassert.Equal(t, ok, false)\n\n\tos.Setenv(\"TEST_ENV_FIELD\", \"1234abcd\")\n\tcfg.LoadEnvForStruct(&envTest{})\n\tv := cfg[\"target_field\"]\n\tassert.Equal(t, v, \"1234abcd\")\n}\n"
  },
  {
    "path": "htpasswd.go",
    "content": "package main\n\nimport (\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"encoding/csv\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\n// Lookup passwords in a htpasswd file\n// Passwords must be generated with -B for bcrypt or -s for SHA1.\n\ntype HtpasswdFile struct {\n\tUsers map[string]string\n}\n\nfunc NewHtpasswdFromFile(path string) (*HtpasswdFile, error) {\n\tr, err := os.Open(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer r.Close()\n\treturn NewHtpasswd(r)\n}\n\nfunc NewHtpasswd(file io.Reader) (*HtpasswdFile, error) {\n\tcsv_reader := csv.NewReader(file)\n\tcsv_reader.Comma = ':'\n\tcsv_reader.Comment = '#'\n\tcsv_reader.TrimLeadingSpace = true\n\n\trecords, err := csv_reader.ReadAll()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\th := &HtpasswdFile{Users: make(map[string]string)}\n\tfor _, record := range records {\n\t\th.Users[record[0]] = record[1]\n\t}\n\treturn h, nil\n}\n\nfunc (h *HtpasswdFile) Validate(user string, password string) bool {\n\trealPassword, exists := h.Users[user]\n\tif !exists {\n\t\treturn false\n\t}\n\n\tshaPrefix := realPassword[:5]\n\tif shaPrefix == \"{SHA}\" {\n\t\tshaValue := realPassword[5:]\n\t\td := sha1.New()\n\t\td.Write([]byte(password))\n\t\treturn shaValue == base64.StdEncoding.EncodeToString(d.Sum(nil))\n\t}\n\n\tbcryptPrefix := realPassword[:4]\n\tif bcryptPrefix == \"$2a$\" || bcryptPrefix == \"$2b$\" || bcryptPrefix == \"$2x$\" || bcryptPrefix == \"$2y$\" {\n\t\treturn bcrypt.CompareHashAndPassword([]byte(realPassword), []byte(password)) == nil\n\t}\n\n\tlog.Printf(\"Invalid htpasswd entry for %s. Must be a SHA or bcrypt entry.\", user)\n\treturn false\n}\n"
  },
  {
    "path": "htpasswd_test.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\nfunc TestSHA(t *testing.T) {\n\tfile := bytes.NewBuffer([]byte(\"testuser:{SHA}PaVBVZkYqAjCQCu6UBL2xgsnZhw=\\n\"))\n\th, err := NewHtpasswd(file)\n\tassert.Equal(t, err, nil)\n\n\tvalid := h.Validate(\"testuser\", \"asdf\")\n\tassert.Equal(t, valid, true)\n}\n\nfunc TestBcrypt(t *testing.T) {\n\thash1, err := bcrypt.GenerateFromPassword([]byte(\"password\"), 1)\n\thash2, err := bcrypt.GenerateFromPassword([]byte(\"top-secret\"), 2)\n\tassert.Equal(t, err, nil)\n\n\tcontents := fmt.Sprintf(\"testuser1:%s\\ntestuser2:%s\\n\", hash1, hash2)\n\tfile := bytes.NewBuffer([]byte(contents))\n\n\th, err := NewHtpasswd(file)\n\tassert.Equal(t, err, nil)\n\n\tvalid := h.Validate(\"testuser1\", \"password\")\n\tassert.Equal(t, valid, true)\n\n\tvalid = h.Validate(\"testuser2\", \"top-secret\")\n\tassert.Equal(t, valid, true)\n}\n"
  },
  {
    "path": "http.go",
    "content": "package main\n\nimport (\n\t\"crypto/tls\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype Server struct {\n\tHandler http.Handler\n\tOpts    *Options\n}\n\nfunc (s *Server) ListenAndServe() {\n\tif s.Opts.TLSKeyFile != \"\" || s.Opts.TLSCertFile != \"\" {\n\t\ts.ServeHTTPS()\n\t} else {\n\t\ts.ServeHTTP()\n\t}\n}\n\nfunc (s *Server) ServeHTTP() {\n\thttpAddress := s.Opts.HttpAddress\n\tscheme := \"\"\n\n\ti := strings.Index(httpAddress, \"://\")\n\tif i > -1 {\n\t\tscheme = httpAddress[0:i]\n\t}\n\n\tvar networkType string\n\tswitch scheme {\n\tcase \"\", \"http\":\n\t\tnetworkType = \"tcp\"\n\tdefault:\n\t\tnetworkType = scheme\n\t}\n\n\tslice := strings.SplitN(httpAddress, \"//\", 2)\n\tlistenAddr := slice[len(slice)-1]\n\n\tlistener, err := net.Listen(networkType, listenAddr)\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: listen (%s, %s) failed - %s\", networkType, listenAddr, err)\n\t}\n\tlog.Printf(\"HTTP: listening on %s\", listenAddr)\n\n\tserver := &http.Server{Handler: s.Handler}\n\terr = server.Serve(listener)\n\tif err != nil && !strings.Contains(err.Error(), \"use of closed network connection\") {\n\t\tlog.Printf(\"ERROR: http.Serve() - %s\", err)\n\t}\n\n\tlog.Printf(\"HTTP: closing %s\", listener.Addr())\n}\n\nfunc (s *Server) ServeHTTPS() {\n\taddr := s.Opts.HttpsAddress\n\tconfig := &tls.Config{\n\t\tMinVersion: tls.VersionTLS12,\n\t\tMaxVersion: tls.VersionTLS12,\n\t}\n\tif config.NextProtos == nil {\n\t\tconfig.NextProtos = []string{\"http/1.1\"}\n\t}\n\n\tvar err error\n\tconfig.Certificates = make([]tls.Certificate, 1)\n\tconfig.Certificates[0], err = tls.LoadX509KeyPair(s.Opts.TLSCertFile, s.Opts.TLSKeyFile)\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: loading tls config (%s, %s) failed - %s\", s.Opts.TLSCertFile, s.Opts.TLSKeyFile, err)\n\t}\n\n\tln, err := net.Listen(\"tcp\", addr)\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: listen (%s) failed - %s\", addr, err)\n\t}\n\tlog.Printf(\"HTTPS: listening on %s\", ln.Addr())\n\n\ttlsListener := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, config)\n\tsrv := &http.Server{Handler: s.Handler}\n\terr = srv.Serve(tlsListener)\n\n\tif err != nil && !strings.Contains(err.Error(), \"use of closed network connection\") {\n\t\tlog.Printf(\"ERROR: https.Serve() - %s\", err)\n\t}\n\n\tlog.Printf(\"HTTPS: closing %s\", tlsListener.Addr())\n}\n\n// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted\n// connections. It's used by ListenAndServe and ListenAndServeTLS so\n// dead TCP connections (e.g. closing laptop mid-download) eventually\n// go away.\ntype tcpKeepAliveListener struct {\n\t*net.TCPListener\n}\n\nfunc (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {\n\ttc, err := ln.AcceptTCP()\n\tif err != nil {\n\t\treturn\n\t}\n\ttc.SetKeepAlive(true)\n\ttc.SetKeepAlivePeriod(3 * time.Minute)\n\treturn tc, nil\n}\n"
  },
  {
    "path": "logging_handler.go",
    "content": "// largely adapted from https://github.com/gorilla/handlers/blob/master/handlers.go\n// to add logging of request duration as last value (and drop referrer)\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"text/template\"\n\t\"time\"\n)\n\nconst (\n\tdefaultRequestLoggingFormat = \"{{.Client}} - {{.Username}} [{{.Timestamp}}] {{.Host}} {{.RequestMethod}} {{.Upstream}} {{.RequestURI}} {{.Protocol}} {{.UserAgent}} {{.StatusCode}} {{.ResponseSize}} {{.RequestDuration}}\"\n)\n\n// responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP status\n// code and body size\ntype responseLogger struct {\n\tw        http.ResponseWriter\n\tstatus   int\n\tsize     int\n\tupstream string\n\tauthInfo string\n}\n\nfunc (l *responseLogger) Header() http.Header {\n\treturn l.w.Header()\n}\n\nfunc (l *responseLogger) ExtractGAPMetadata() {\n\tupstream := l.w.Header().Get(\"GAP-Upstream-Address\")\n\tif upstream != \"\" {\n\t\tl.upstream = upstream\n\t\tl.w.Header().Del(\"GAP-Upstream-Address\")\n\t}\n\tauthInfo := l.w.Header().Get(\"GAP-Auth\")\n\tif authInfo != \"\" {\n\t\tl.authInfo = authInfo\n\t\tl.w.Header().Del(\"GAP-Auth\")\n\t}\n}\n\nfunc (l *responseLogger) Write(b []byte) (int, error) {\n\tif l.status == 0 {\n\t\t// The status will be StatusOK if WriteHeader has not been called yet\n\t\tl.status = http.StatusOK\n\t}\n\tl.ExtractGAPMetadata()\n\tsize, err := l.w.Write(b)\n\tl.size += size\n\treturn size, err\n}\n\nfunc (l *responseLogger) WriteHeader(s int) {\n\tl.ExtractGAPMetadata()\n\tl.w.WriteHeader(s)\n\tl.status = s\n}\n\nfunc (l *responseLogger) Status() int {\n\treturn l.status\n}\n\nfunc (l *responseLogger) Size() int {\n\treturn l.size\n}\n\n// logMessageData is the container for all values that are available as variables in the request logging format.\n// All values are pre-formatted strings so it is easy to use them in the format string.\ntype logMessageData struct {\n\tClient,\n\tHost,\n\tProtocol,\n\tRequestDuration,\n\tRequestMethod,\n\tRequestURI,\n\tResponseSize,\n\tStatusCode,\n\tTimestamp,\n\tUpstream,\n\tUserAgent,\n\tUsername string\n}\n\n// loggingHandler is the http.Handler implementation for LoggingHandlerTo and its friends\ntype loggingHandler struct {\n\twriter      io.Writer\n\thandler     http.Handler\n\tenabled     bool\n\tlogTemplate *template.Template\n}\n\nfunc LoggingHandler(out io.Writer, h http.Handler, v bool, requestLoggingTpl string) http.Handler {\n\treturn loggingHandler{\n\t\twriter:      out,\n\t\thandler:     h,\n\t\tenabled:     v,\n\t\tlogTemplate: template.Must(template.New(\"request-log\").Parse(requestLoggingTpl)),\n\t}\n}\n\nfunc (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tt := time.Now()\n\turl := *req.URL\n\tlogger := &responseLogger{w: w}\n\th.handler.ServeHTTP(logger, req)\n\tif !h.enabled {\n\t\treturn\n\t}\n\th.writeLogLine(logger.authInfo, logger.upstream, req, url, t, logger.Status(), logger.Size())\n}\n\n// Log entry for req similar to Apache Common Log Format.\n// ts is the timestamp with which the entry should be logged.\n// status, size are used to provide the response HTTP status and size.\nfunc (h loggingHandler) writeLogLine(username, upstream string, req *http.Request, url url.URL, ts time.Time, status int, size int) {\n\tif username == \"\" {\n\t\tusername = \"-\"\n\t}\n\tif upstream == \"\" {\n\t\tupstream = \"-\"\n\t}\n\tif url.User != nil && username == \"-\" {\n\t\tif name := url.User.Username(); name != \"\" {\n\t\t\tusername = name\n\t\t}\n\t}\n\n\tclient := req.Header.Get(\"X-Real-IP\")\n\tif client == \"\" {\n\t\tclient = req.RemoteAddr\n\t}\n\n\tif c, _, err := net.SplitHostPort(client); err == nil {\n\t\tclient = c\n\t}\n\n\tduration := float64(time.Now().Sub(ts)) / float64(time.Second)\n\n\th.logTemplate.Execute(h.writer, logMessageData{\n\t\tClient:          client,\n\t\tHost:            req.Host,\n\t\tProtocol:        req.Proto,\n\t\tRequestDuration: fmt.Sprintf(\"%0.3f\", duration),\n\t\tRequestMethod:   req.Method,\n\t\tRequestURI:      fmt.Sprintf(\"%q\", url.RequestURI()),\n\t\tResponseSize:    fmt.Sprintf(\"%d\", size),\n\t\tStatusCode:      fmt.Sprintf(\"%d\", status),\n\t\tTimestamp:       ts.Format(\"02/Jan/2006:15:04:05 -0700\"),\n\t\tUpstream:        upstream,\n\t\tUserAgent:       fmt.Sprintf(\"%q\", req.UserAgent()),\n\t\tUsername:        username,\n\t})\n\n\th.writer.Write([]byte(\"\\n\"))\n}\n"
  },
  {
    "path": "logging_handler_test.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestLoggingHandler_ServeHTTP(t *testing.T) {\n\tts := time.Now()\n\n\ttests := []struct {\n\t\tFormat,\n\t\tExpectedLogMessage string\n\t}{\n\t\t{defaultRequestLoggingFormat, fmt.Sprintf(\"127.0.0.1 - - [%s] test-server GET - \\\"/foo/bar\\\" HTTP/1.1 \\\"\\\" 200 4 0.000\\n\", ts.Format(\"02/Jan/2006:15:04:05 -0700\"))},\n\t\t{\"{{.RequestMethod}}\", \"GET\\n\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tbuf := bytes.NewBuffer(nil)\n\t\thandler := func(w http.ResponseWriter, req *http.Request) {\n\t\t\tw.Write([]byte(\"test\"))\n\t\t}\n\n\t\th := LoggingHandler(buf, http.HandlerFunc(handler), true, test.Format)\n\n\t\tr, _ := http.NewRequest(\"GET\", \"/foo/bar\", nil)\n\t\tr.RemoteAddr = \"127.0.0.1\"\n\t\tr.Host = \"test-server\"\n\n\t\th.ServeHTTP(httptest.NewRecorder(), r)\n\n\t\tactual := buf.String()\n\t\tif actual != test.ExpectedLogMessage {\n\t\t\tt.Errorf(\"Log message was\\n%s\\ninstead of expected \\n%s\", actual, test.ExpectedLogMessage)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/BurntSushi/toml\"\n\t\"github.com/mreiferson/go-options\"\n)\n\nfunc main() {\n\tlog.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)\n\tflagSet := flag.NewFlagSet(\"oauth2_proxy\", flag.ExitOnError)\n\n\temailDomains := StringArray{}\n\tupstreams := StringArray{}\n\tskipAuthRegex := StringArray{}\n\tgoogleGroups := StringArray{}\n\n\tconfig := flagSet.String(\"config\", \"\", \"path to config file\")\n\tshowVersion := flagSet.Bool(\"version\", false, \"print version string\")\n\n\tflagSet.String(\"http-address\", \"127.0.0.1:4180\", \"[http://]<addr>:<port> or unix://<path> to listen on for HTTP clients\")\n\tflagSet.String(\"https-address\", \":443\", \"<addr>:<port> to listen on for HTTPS clients\")\n\tflagSet.String(\"tls-cert\", \"\", \"path to certificate file\")\n\tflagSet.String(\"tls-key\", \"\", \"path to private key file\")\n\tflagSet.String(\"redirect-url\", \"\", \"the OAuth Redirect URL. ie: \\\"https://internalapp.yourcompany.com/oauth2/callback\\\"\")\n\tflagSet.Bool(\"set-xauthrequest\", false, \"set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)\")\n\tflagSet.Var(&upstreams, \"upstream\", \"the http url(s) of the upstream endpoint or file:// paths for static files. Routing is based on the path\")\n\tflagSet.Bool(\"pass-basic-auth\", true, \"pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream\")\n\tflagSet.Bool(\"pass-user-headers\", true, \"pass X-Forwarded-User and X-Forwarded-Email information to upstream\")\n\tflagSet.String(\"basic-auth-password\", \"\", \"the password to set when passing the HTTP Basic Auth header\")\n\tflagSet.Bool(\"pass-access-token\", false, \"pass OAuth access_token to upstream via X-Forwarded-Access-Token header\")\n\tflagSet.Bool(\"pass-host-header\", true, \"pass the request Host Header to upstream\")\n\tflagSet.Var(&skipAuthRegex, \"skip-auth-regex\", \"bypass authentication for requests path's that match (may be given multiple times)\")\n\tflagSet.Bool(\"skip-provider-button\", false, \"will skip sign-in-page to directly reach the next step: oauth/start\")\n\tflagSet.Bool(\"skip-auth-preflight\", false, \"will skip authentication for OPTIONS requests\")\n\tflagSet.Bool(\"ssl-insecure-skip-verify\", false, \"skip validation of certificates presented when using HTTPS\")\n\n\tflagSet.Var(&emailDomains, \"email-domain\", \"authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email\")\n\tflagSet.String(\"azure-tenant\", \"common\", \"go to a tenant-specific or common (tenant-independent) endpoint.\")\n\tflagSet.String(\"github-org\", \"\", \"restrict logins to members of this organisation\")\n\tflagSet.String(\"github-team\", \"\", \"restrict logins to members of this team\")\n\tflagSet.Var(&googleGroups, \"google-group\", \"restrict logins to members of this google group (may be given multiple times).\")\n\tflagSet.String(\"google-admin-email\", \"\", \"the google admin to impersonate for api calls\")\n\tflagSet.String(\"google-service-account-json\", \"\", \"the path to the service account json credentials\")\n\tflagSet.String(\"client-id\", \"\", \"the OAuth Client ID: ie: \\\"123456.apps.googleusercontent.com\\\"\")\n\tflagSet.String(\"client-secret\", \"\", \"the OAuth Client Secret\")\n\tflagSet.String(\"authenticated-emails-file\", \"\", \"authenticate against emails via file (one per line)\")\n\tflagSet.String(\"htpasswd-file\", \"\", \"additionally authenticate against a htpasswd file. Entries must be created with \\\"htpasswd -s\\\" for SHA encryption or \\\"htpasswd -B\\\" for bcrypt encryption\")\n\tflagSet.Bool(\"display-htpasswd-form\", true, \"display username / password login form if an htpasswd file is provided\")\n\tflagSet.String(\"custom-templates-dir\", \"\", \"path to custom html templates\")\n\tflagSet.String(\"footer\", \"\", \"custom footer string. Use \\\"-\\\" to disable default footer.\")\n\tflagSet.String(\"proxy-prefix\", \"/oauth2\", \"the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)\")\n\n\tflagSet.String(\"cookie-name\", \"_oauth2_proxy\", \"the name of the cookie that the oauth_proxy creates\")\n\tflagSet.String(\"cookie-secret\", \"\", \"the seed string for secure cookies (optionally base64 encoded)\")\n\tflagSet.String(\"cookie-domain\", \"\", \"an optional cookie domain to force cookies to (ie: .yourcompany.com)*\")\n\tflagSet.Duration(\"cookie-expire\", time.Duration(168)*time.Hour, \"expire timeframe for cookie\")\n\tflagSet.Duration(\"cookie-refresh\", time.Duration(0), \"refresh the cookie after this duration; 0 to disable\")\n\tflagSet.Bool(\"cookie-secure\", true, \"set secure (HTTPS) cookie flag\")\n\tflagSet.Bool(\"cookie-httponly\", true, \"set HttpOnly cookie flag\")\n\n\tflagSet.Bool(\"request-logging\", true, \"Log requests to stdout\")\n\tflagSet.String(\"request-logging-format\", defaultRequestLoggingFormat, \"Template for log lines\")\n\n\tflagSet.String(\"provider\", \"google\", \"OAuth provider\")\n\tflagSet.String(\"oidc-issuer-url\", \"\", \"OpenID Connect issuer URL (ie: https://accounts.google.com)\")\n\tflagSet.String(\"login-url\", \"\", \"Authentication endpoint\")\n\tflagSet.String(\"redeem-url\", \"\", \"Token redemption endpoint\")\n\tflagSet.String(\"profile-url\", \"\", \"Profile access endpoint\")\n\tflagSet.String(\"resource\", \"\", \"The resource that is protected (Azure AD only)\")\n\tflagSet.String(\"validate-url\", \"\", \"Access token validation endpoint\")\n\tflagSet.String(\"scope\", \"\", \"OAuth scope specification\")\n\tflagSet.String(\"approval-prompt\", \"force\", \"OAuth approval_prompt\")\n\n\tflagSet.String(\"signature-key\", \"\", \"GAP-Signature request signature key (algorithm:secretkey)\")\n\n\tflagSet.Parse(os.Args[1:])\n\n\tif *showVersion {\n\t\tfmt.Printf(\"oauth2_proxy v%s (built with %s)\\n\", VERSION, runtime.Version())\n\t\treturn\n\t}\n\n\topts := NewOptions()\n\n\tcfg := make(EnvOptions)\n\tif *config != \"\" {\n\t\t_, err := toml.DecodeFile(*config, &cfg)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"ERROR: failed to load config file %s - %s\", *config, err)\n\t\t}\n\t}\n\tcfg.LoadEnvForStruct(opts)\n\toptions.Resolve(opts, flagSet, cfg)\n\n\terr := opts.Validate()\n\tif err != nil {\n\t\tlog.Printf(\"%s\", err)\n\t\tos.Exit(1)\n\t}\n\tvalidator := NewValidator(opts.EmailDomains, opts.AuthenticatedEmailsFile)\n\toauthproxy := NewOAuthProxy(opts, validator)\n\n\tif len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == \"\" {\n\t\tif len(opts.EmailDomains) > 1 {\n\t\t\toauthproxy.SignInMessage = fmt.Sprintf(\"Authenticate using one of the following domains: %v\", strings.Join(opts.EmailDomains, \", \"))\n\t\t} else if opts.EmailDomains[0] != \"*\" {\n\t\t\toauthproxy.SignInMessage = fmt.Sprintf(\"Authenticate using %v\", opts.EmailDomains[0])\n\t\t}\n\t}\n\n\tif opts.HtpasswdFile != \"\" {\n\t\tlog.Printf(\"using htpasswd file %s\", opts.HtpasswdFile)\n\t\toauthproxy.HtpasswdFile, err = NewHtpasswdFromFile(opts.HtpasswdFile)\n\t\toauthproxy.DisplayHtpasswdForm = opts.DisplayHtpasswdForm\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"FATAL: unable to open %s %s\", opts.HtpasswdFile, err)\n\t\t}\n\t}\n\n\ts := &Server{\n\t\tHandler: LoggingHandler(os.Stdout, oauthproxy, opts.RequestLogging, opts.RequestLoggingFormat),\n\t\tOpts:    opts,\n\t}\n\ts.ListenAndServe()\n}\n"
  },
  {
    "path": "oauthproxy.go",
    "content": "package main\n\nimport (\n\tb64 \"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bitly/oauth2_proxy/cookie\"\n\t\"github.com/bitly/oauth2_proxy/providers\"\n\t\"github.com/mbland/hmacauth\"\n)\n\nconst SignatureHeader = \"GAP-Signature\"\n\nvar SignatureHeaders []string = []string{\n\t\"Content-Length\",\n\t\"Content-Md5\",\n\t\"Content-Type\",\n\t\"Date\",\n\t\"Authorization\",\n\t\"X-Forwarded-User\",\n\t\"X-Forwarded-Email\",\n\t\"X-Forwarded-Access-Token\",\n\t\"Cookie\",\n\t\"Gap-Auth\",\n}\n\ntype OAuthProxy struct {\n\tCookieSeed     string\n\tCookieName     string\n\tCSRFCookieName string\n\tCookieDomain   string\n\tCookieSecure   bool\n\tCookieHttpOnly bool\n\tCookieExpire   time.Duration\n\tCookieRefresh  time.Duration\n\tValidator      func(string) bool\n\n\tRobotsPath        string\n\tPingPath          string\n\tSignInPath        string\n\tSignOutPath       string\n\tOAuthStartPath    string\n\tOAuthCallbackPath string\n\tAuthOnlyPath      string\n\n\tredirectURL         *url.URL // the url to receive requests at\n\tprovider            providers.Provider\n\tProxyPrefix         string\n\tSignInMessage       string\n\tHtpasswdFile        *HtpasswdFile\n\tDisplayHtpasswdForm bool\n\tserveMux            http.Handler\n\tSetXAuthRequest     bool\n\tPassBasicAuth       bool\n\tSkipProviderButton  bool\n\tPassUserHeaders     bool\n\tBasicAuthPassword   string\n\tPassAccessToken     bool\n\tCookieCipher        *cookie.Cipher\n\tskipAuthRegex       []string\n\tskipAuthPreflight   bool\n\tcompiledRegex       []*regexp.Regexp\n\ttemplates           *template.Template\n\tFooter              string\n}\n\ntype UpstreamProxy struct {\n\tupstream string\n\thandler  http.Handler\n\tauth     hmacauth.HmacAuth\n}\n\nfunc (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"GAP-Upstream-Address\", u.upstream)\n\tif u.auth != nil {\n\t\tr.Header.Set(\"GAP-Auth\", w.Header().Get(\"GAP-Auth\"))\n\t\tu.auth.SignRequest(r)\n\t}\n\tu.handler.ServeHTTP(w, r)\n}\n\nfunc NewReverseProxy(target *url.URL) (proxy *httputil.ReverseProxy) {\n\treturn httputil.NewSingleHostReverseProxy(target)\n}\nfunc setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {\n\tdirector := proxy.Director\n\tproxy.Director = func(req *http.Request) {\n\t\tdirector(req)\n\t\t// use RequestURI so that we aren't unescaping encoded slashes in the request path\n\t\treq.Host = target.Host\n\t\treq.URL.Opaque = req.RequestURI\n\t\treq.URL.RawQuery = \"\"\n\t}\n}\nfunc setProxyDirector(proxy *httputil.ReverseProxy) {\n\tdirector := proxy.Director\n\tproxy.Director = func(req *http.Request) {\n\t\tdirector(req)\n\t\t// use RequestURI so that we aren't unescaping encoded slashes in the request path\n\t\treq.URL.Opaque = req.RequestURI\n\t\treq.URL.RawQuery = \"\"\n\t}\n}\nfunc NewFileServer(path string, filesystemPath string) (proxy http.Handler) {\n\treturn http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath)))\n}\n\nfunc NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {\n\tserveMux := http.NewServeMux()\n\tvar auth hmacauth.HmacAuth\n\tif sigData := opts.signatureData; sigData != nil {\n\t\tauth = hmacauth.NewHmacAuth(sigData.hash, []byte(sigData.key),\n\t\t\tSignatureHeader, SignatureHeaders)\n\t}\n\tfor _, u := range opts.proxyURLs {\n\t\tpath := u.Path\n\t\tswitch u.Scheme {\n\t\tcase \"http\", \"https\":\n\t\t\tu.Path = \"\"\n\t\t\tlog.Printf(\"mapping path %q => upstream %q\", path, u)\n\t\t\tproxy := NewReverseProxy(u)\n\t\t\tif !opts.PassHostHeader {\n\t\t\t\tsetProxyUpstreamHostHeader(proxy, u)\n\t\t\t} else {\n\t\t\t\tsetProxyDirector(proxy)\n\t\t\t}\n\t\t\tserveMux.Handle(path,\n\t\t\t\t&UpstreamProxy{u.Host, proxy, auth})\n\t\tcase \"file\":\n\t\t\tif u.Fragment != \"\" {\n\t\t\t\tpath = u.Fragment\n\t\t\t}\n\t\t\tlog.Printf(\"mapping path %q => file system %q\", path, u.Path)\n\t\t\tproxy := NewFileServer(path, u.Path)\n\t\t\tserveMux.Handle(path, &UpstreamProxy{path, proxy, nil})\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unknown upstream protocol %s\", u.Scheme))\n\t\t}\n\t}\n\tfor _, u := range opts.CompiledRegex {\n\t\tlog.Printf(\"compiled skip-auth-regex => %q\", u)\n\t}\n\n\tredirectURL := opts.redirectURL\n\tredirectURL.Path = fmt.Sprintf(\"%s/callback\", opts.ProxyPrefix)\n\n\tlog.Printf(\"OAuthProxy configured for %s Client ID: %s\", opts.provider.Data().ProviderName, opts.ClientID)\n\trefresh := \"disabled\"\n\tif opts.CookieRefresh != time.Duration(0) {\n\t\trefresh = fmt.Sprintf(\"after %s\", opts.CookieRefresh)\n\t}\n\n\tlog.Printf(\"Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s refresh:%s\", opts.CookieName, opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, opts.CookieDomain, refresh)\n\n\tvar cipher *cookie.Cipher\n\tif opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) {\n\t\tvar err error\n\t\tcipher, err = cookie.NewCipher(secretBytes(opts.CookieSecret))\n\t\tif err != nil {\n\t\t\tlog.Fatal(\"cookie-secret error: \", err)\n\t\t}\n\t}\n\n\treturn &OAuthProxy{\n\t\tCookieName:     opts.CookieName,\n\t\tCSRFCookieName: fmt.Sprintf(\"%v_%v\", opts.CookieName, \"csrf\"),\n\t\tCookieSeed:     opts.CookieSecret,\n\t\tCookieDomain:   opts.CookieDomain,\n\t\tCookieSecure:   opts.CookieSecure,\n\t\tCookieHttpOnly: opts.CookieHttpOnly,\n\t\tCookieExpire:   opts.CookieExpire,\n\t\tCookieRefresh:  opts.CookieRefresh,\n\t\tValidator:      validator,\n\n\t\tRobotsPath:        \"/robots.txt\",\n\t\tPingPath:          \"/ping\",\n\t\tSignInPath:        fmt.Sprintf(\"%s/sign_in\", opts.ProxyPrefix),\n\t\tSignOutPath:       fmt.Sprintf(\"%s/sign_out\", opts.ProxyPrefix),\n\t\tOAuthStartPath:    fmt.Sprintf(\"%s/start\", opts.ProxyPrefix),\n\t\tOAuthCallbackPath: fmt.Sprintf(\"%s/callback\", opts.ProxyPrefix),\n\t\tAuthOnlyPath:      fmt.Sprintf(\"%s/auth\", opts.ProxyPrefix),\n\n\t\tProxyPrefix:        opts.ProxyPrefix,\n\t\tprovider:           opts.provider,\n\t\tserveMux:           serveMux,\n\t\tredirectURL:        redirectURL,\n\t\tskipAuthRegex:      opts.SkipAuthRegex,\n\t\tskipAuthPreflight:  opts.SkipAuthPreflight,\n\t\tcompiledRegex:      opts.CompiledRegex,\n\t\tSetXAuthRequest:    opts.SetXAuthRequest,\n\t\tPassBasicAuth:      opts.PassBasicAuth,\n\t\tPassUserHeaders:    opts.PassUserHeaders,\n\t\tBasicAuthPassword:  opts.BasicAuthPassword,\n\t\tPassAccessToken:    opts.PassAccessToken,\n\t\tSkipProviderButton: opts.SkipProviderButton,\n\t\tCookieCipher:       cipher,\n\t\ttemplates:          loadTemplates(opts.CustomTemplatesDir),\n\t\tFooter:             opts.Footer,\n\t}\n}\n\nfunc (p *OAuthProxy) GetRedirectURI(host string) string {\n\t// default to the request Host if not set\n\tif p.redirectURL.Host != \"\" {\n\t\treturn p.redirectURL.String()\n\t}\n\tvar u url.URL\n\tu = *p.redirectURL\n\tif u.Scheme == \"\" {\n\t\tif p.CookieSecure {\n\t\t\tu.Scheme = \"https\"\n\t\t} else {\n\t\t\tu.Scheme = \"http\"\n\t\t}\n\t}\n\tu.Host = host\n\treturn u.String()\n}\n\nfunc (p *OAuthProxy) displayCustomLoginForm() bool {\n\treturn p.HtpasswdFile != nil && p.DisplayHtpasswdForm\n}\n\nfunc (p *OAuthProxy) redeemCode(host, code string) (s *providers.SessionState, err error) {\n\tif code == \"\" {\n\t\treturn nil, errors.New(\"missing code\")\n\t}\n\tredirectURI := p.GetRedirectURI(host)\n\ts, err = p.provider.Redeem(redirectURI, code)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif s.Email == \"\" {\n\t\ts.Email, err = p.provider.GetEmailAddress(s)\n\t}\n\n\tif s.User == \"\" {\n\t\ts.User, err = p.provider.GetUserName(s)\n\t\tif err != nil && err.Error() == \"not implemented\" {\n\t\t\terr = nil\n\t\t}\n\t}\n\treturn\n}\n\nfunc (p *OAuthProxy) MakeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {\n\tif value != \"\" {\n\t\tvalue = cookie.SignedValue(p.CookieSeed, p.CookieName, value, now)\n\t\tif len(value) > 4096 {\n\t\t\t// Cookies cannot be larger than 4kb\n\t\t\tlog.Printf(\"WARNING - Cookie Size: %d bytes\", len(value))\n\t\t}\n\t}\n\treturn p.makeCookie(req, p.CookieName, value, expiration, now)\n}\n\nfunc (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {\n\treturn p.makeCookie(req, p.CSRFCookieName, value, expiration, now)\n}\n\nfunc (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {\n\tif p.CookieDomain != \"\" {\n\t\tdomain := req.Host\n\t\tif h, _, err := net.SplitHostPort(domain); err == nil {\n\t\t\tdomain = h\n\t\t}\n\t\tif !strings.HasSuffix(domain, p.CookieDomain) {\n\t\t\tlog.Printf(\"Warning: request host is %q but using configured cookie domain of %q\", domain, p.CookieDomain)\n\t\t}\n\t}\n\n\treturn &http.Cookie{\n\t\tName:     name,\n\t\tValue:    value,\n\t\tPath:     \"/\",\n\t\tDomain:   p.CookieDomain,\n\t\tHttpOnly: p.CookieHttpOnly,\n\t\tSecure:   p.CookieSecure,\n\t\tExpires:  now.Add(expiration),\n\t}\n}\n\nfunc (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {\n\thttp.SetCookie(rw, p.MakeCSRFCookie(req, \"\", time.Hour*-1, time.Now()))\n}\n\nfunc (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {\n\thttp.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))\n}\n\nfunc (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) {\n\tclr := p.MakeSessionCookie(req, \"\", time.Hour*-1, time.Now())\n\thttp.SetCookie(rw, clr)\n\n\t// ugly hack because default domain changed\n\tif p.CookieDomain == \"\" {\n\t\tclr2 := *clr\n\t\tclr2.Domain = req.Host\n\t\thttp.SetCookie(rw, &clr2)\n\t}\n}\n\nfunc (p *OAuthProxy) SetSessionCookie(rw http.ResponseWriter, req *http.Request, val string) {\n\thttp.SetCookie(rw, p.MakeSessionCookie(req, val, p.CookieExpire, time.Now()))\n}\n\nfunc (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*providers.SessionState, time.Duration, error) {\n\tvar age time.Duration\n\tc, err := req.Cookie(p.CookieName)\n\tif err != nil {\n\t\t// always http.ErrNoCookie\n\t\treturn nil, age, fmt.Errorf(\"Cookie %q not present\", p.CookieName)\n\t}\n\tval, timestamp, ok := cookie.Validate(c, p.CookieSeed, p.CookieExpire)\n\tif !ok {\n\t\treturn nil, age, errors.New(\"Cookie Signature not valid\")\n\t}\n\n\tsession, err := p.provider.SessionFromCookie(val, p.CookieCipher)\n\tif err != nil {\n\t\treturn nil, age, err\n\t}\n\n\tage = time.Now().Truncate(time.Second).Sub(timestamp)\n\treturn session, age, nil\n}\n\nfunc (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *providers.SessionState) error {\n\tvalue, err := p.provider.CookieForSession(s, p.CookieCipher)\n\tif err != nil {\n\t\treturn err\n\t}\n\tp.SetSessionCookie(rw, req, value)\n\treturn nil\n}\n\nfunc (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {\n\trw.WriteHeader(http.StatusOK)\n\tfmt.Fprintf(rw, \"User-agent: *\\nDisallow: /\")\n}\n\nfunc (p *OAuthProxy) PingPage(rw http.ResponseWriter) {\n\trw.WriteHeader(http.StatusOK)\n\tfmt.Fprintf(rw, \"OK\")\n}\n\nfunc (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {\n\tlog.Printf(\"ErrorPage %d %s %s\", code, title, message)\n\trw.WriteHeader(code)\n\tt := struct {\n\t\tTitle       string\n\t\tMessage     string\n\t\tProxyPrefix string\n\t}{\n\t\tTitle:       fmt.Sprintf(\"%d %s\", code, title),\n\t\tMessage:     message,\n\t\tProxyPrefix: p.ProxyPrefix,\n\t}\n\tp.templates.ExecuteTemplate(rw, \"error.html\", t)\n}\n\nfunc (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {\n\tp.ClearSessionCookie(rw, req)\n\trw.WriteHeader(code)\n\n\tredirect_url := req.URL.RequestURI()\n\tif req.Header.Get(\"X-Auth-Request-Redirect\") != \"\" {\n\t\tredirect_url = req.Header.Get(\"X-Auth-Request-Redirect\")\n\t}\n\tif redirect_url == p.SignInPath {\n\t\tredirect_url = \"/\"\n\t}\n\n\tt := struct {\n\t\tProviderName  string\n\t\tSignInMessage string\n\t\tCustomLogin   bool\n\t\tRedirect      string\n\t\tVersion       string\n\t\tProxyPrefix   string\n\t\tFooter        template.HTML\n\t}{\n\t\tProviderName:  p.provider.Data().ProviderName,\n\t\tSignInMessage: p.SignInMessage,\n\t\tCustomLogin:   p.displayCustomLoginForm(),\n\t\tRedirect:      redirect_url,\n\t\tVersion:       VERSION,\n\t\tProxyPrefix:   p.ProxyPrefix,\n\t\tFooter:        template.HTML(p.Footer),\n\t}\n\tp.templates.ExecuteTemplate(rw, \"sign_in.html\", t)\n}\n\nfunc (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (string, bool) {\n\tif req.Method != \"POST\" || p.HtpasswdFile == nil {\n\t\treturn \"\", false\n\t}\n\tuser := req.FormValue(\"username\")\n\tpasswd := req.FormValue(\"password\")\n\tif user == \"\" {\n\t\treturn \"\", false\n\t}\n\t// check auth\n\tif p.HtpasswdFile.Validate(user, passwd) {\n\t\tlog.Printf(\"authenticated %q via HtpasswdFile\", user)\n\t\treturn user, true\n\t}\n\treturn \"\", false\n}\n\nfunc (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {\n\terr = req.ParseForm()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tredirect = req.Form.Get(\"rd\")\n\tif redirect == \"\" || !strings.HasPrefix(redirect, \"/\") || strings.HasPrefix(redirect, \"//\") {\n\t\tredirect = \"/\"\n\t}\n\n\treturn\n}\n\nfunc (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) (ok bool) {\n\tisPreflightRequestAllowed := p.skipAuthPreflight && req.Method == \"OPTIONS\"\n\treturn isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path)\n}\n\nfunc (p *OAuthProxy) IsWhitelistedPath(path string) (ok bool) {\n\tfor _, u := range p.compiledRegex {\n\t\tok = u.MatchString(path)\n\t\tif ok {\n\t\t\treturn\n\t\t}\n\t}\n\treturn\n}\n\nfunc getRemoteAddr(req *http.Request) (s string) {\n\ts = req.RemoteAddr\n\tif req.Header.Get(\"X-Real-IP\") != \"\" {\n\t\ts += fmt.Sprintf(\" (%q)\", req.Header.Get(\"X-Real-IP\"))\n\t}\n\treturn\n}\n\nfunc (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {\n\tswitch path := req.URL.Path; {\n\tcase path == p.RobotsPath:\n\t\tp.RobotsTxt(rw)\n\tcase path == p.PingPath:\n\t\tp.PingPage(rw)\n\tcase p.IsWhitelistedRequest(req):\n\t\tp.serveMux.ServeHTTP(rw, req)\n\tcase path == p.SignInPath:\n\t\tp.SignIn(rw, req)\n\tcase path == p.SignOutPath:\n\t\tp.SignOut(rw, req)\n\tcase path == p.OAuthStartPath:\n\t\tp.OAuthStart(rw, req)\n\tcase path == p.OAuthCallbackPath:\n\t\tp.OAuthCallback(rw, req)\n\tcase path == p.AuthOnlyPath:\n\t\tp.AuthenticateOnly(rw, req)\n\tdefault:\n\t\tp.Proxy(rw, req)\n\t}\n}\n\nfunc (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {\n\tredirect, err := p.GetRedirect(req)\n\tif err != nil {\n\t\tp.ErrorPage(rw, 500, \"Internal Error\", err.Error())\n\t\treturn\n\t}\n\n\tuser, ok := p.ManualSignIn(rw, req)\n\tif ok {\n\t\tsession := &providers.SessionState{User: user}\n\t\tp.SaveSession(rw, req, session)\n\t\thttp.Redirect(rw, req, redirect, 302)\n\t} else {\n\t\tif p.SkipProviderButton {\n\t\t\tp.OAuthStart(rw, req)\n\t\t} else {\n\t\t\tp.SignInPage(rw, req, http.StatusOK)\n\t\t}\n\t}\n}\n\nfunc (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {\n\tp.ClearSessionCookie(rw, req)\n\thttp.Redirect(rw, req, \"/\", 302)\n}\n\nfunc (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {\n\tnonce, err := cookie.Nonce()\n\tif err != nil {\n\t\tp.ErrorPage(rw, 500, \"Internal Error\", err.Error())\n\t\treturn\n\t}\n\tp.SetCSRFCookie(rw, req, nonce)\n\tredirect, err := p.GetRedirect(req)\n\tif err != nil {\n\t\tp.ErrorPage(rw, 500, \"Internal Error\", err.Error())\n\t\treturn\n\t}\n\tredirectURI := p.GetRedirectURI(req.Host)\n\thttp.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf(\"%v:%v\", nonce, redirect)), 302)\n}\n\nfunc (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {\n\tremoteAddr := getRemoteAddr(req)\n\n\t// finish the oauth cycle\n\terr := req.ParseForm()\n\tif err != nil {\n\t\tp.ErrorPage(rw, 500, \"Internal Error\", err.Error())\n\t\treturn\n\t}\n\terrorString := req.Form.Get(\"error\")\n\tif errorString != \"\" {\n\t\tp.ErrorPage(rw, 403, \"Permission Denied\", errorString)\n\t\treturn\n\t}\n\n\tsession, err := p.redeemCode(req.Host, req.Form.Get(\"code\"))\n\tif err != nil {\n\t\tlog.Printf(\"%s error redeeming code %s\", remoteAddr, err)\n\t\tp.ErrorPage(rw, 500, \"Internal Error\", \"Internal Error\")\n\t\treturn\n\t}\n\n\ts := strings.SplitN(req.Form.Get(\"state\"), \":\", 2)\n\tif len(s) != 2 {\n\t\tp.ErrorPage(rw, 500, \"Internal Error\", \"Invalid State\")\n\t\treturn\n\t}\n\tnonce := s[0]\n\tredirect := s[1]\n\tc, err := req.Cookie(p.CSRFCookieName)\n\tif err != nil {\n\t\tp.ErrorPage(rw, 403, \"Permission Denied\", err.Error())\n\t\treturn\n\t}\n\tp.ClearCSRFCookie(rw, req)\n\tif c.Value != nonce {\n\t\tlog.Printf(\"%s csrf token mismatch, potential attack\", remoteAddr)\n\t\tp.ErrorPage(rw, 403, \"Permission Denied\", \"csrf failed\")\n\t\treturn\n\t}\n\n\tif !strings.HasPrefix(redirect, \"/\") || strings.HasPrefix(redirect, \"//\") {\n\t\tredirect = \"/\"\n\t}\n\n\t// set cookie, or deny\n\tif p.Validator(session.Email) && p.provider.ValidateGroup(session.Email) {\n\t\tlog.Printf(\"%s authentication complete %s\", remoteAddr, session)\n\t\terr := p.SaveSession(rw, req, session)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"%s %s\", remoteAddr, err)\n\t\t\tp.ErrorPage(rw, 500, \"Internal Error\", \"Internal Error\")\n\t\t\treturn\n\t\t}\n\t\thttp.Redirect(rw, req, redirect, 302)\n\t} else {\n\t\tlog.Printf(\"%s Permission Denied: %q is unauthorized\", remoteAddr, session.Email)\n\t\tp.ErrorPage(rw, 403, \"Permission Denied\", \"Invalid Account\")\n\t}\n}\n\nfunc (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {\n\tstatus := p.Authenticate(rw, req)\n\tif status == http.StatusAccepted {\n\t\trw.WriteHeader(http.StatusAccepted)\n\t} else {\n\t\thttp.Error(rw, \"unauthorized request\", http.StatusUnauthorized)\n\t}\n}\n\nfunc (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {\n\tstatus := p.Authenticate(rw, req)\n\tif status == http.StatusInternalServerError {\n\t\tp.ErrorPage(rw, http.StatusInternalServerError,\n\t\t\t\"Internal Error\", \"Internal Error\")\n\t} else if status == http.StatusForbidden {\n\t\tif p.SkipProviderButton {\n\t\t\tp.OAuthStart(rw, req)\n\t\t} else {\n\t\t\tp.SignInPage(rw, req, http.StatusForbidden)\n\t\t}\n\t} else {\n\t\tp.serveMux.ServeHTTP(rw, req)\n\t}\n}\n\nfunc (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int {\n\tvar saveSession, clearSession, revalidated bool\n\tremoteAddr := getRemoteAddr(req)\n\n\tsession, sessionAge, err := p.LoadCookiedSession(req)\n\tif err != nil {\n\t\tlog.Printf(\"%s %s\", remoteAddr, err)\n\t}\n\tif session != nil && sessionAge > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {\n\t\tlog.Printf(\"%s refreshing %s old session cookie for %s (refresh after %s)\", remoteAddr, sessionAge, session, p.CookieRefresh)\n\t\tsaveSession = true\n\t}\n\n\tif ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil {\n\t\tlog.Printf(\"%s removing session. error refreshing access token %s %s\", remoteAddr, err, session)\n\t\tclearSession = true\n\t\tsession = nil\n\t} else if ok {\n\t\tsaveSession = true\n\t\trevalidated = true\n\t}\n\n\tif session != nil && session.IsExpired() {\n\t\tlog.Printf(\"%s removing session. token expired %s\", remoteAddr, session)\n\t\tsession = nil\n\t\tsaveSession = false\n\t\tclearSession = true\n\t}\n\n\tif saveSession && !revalidated && session != nil && session.AccessToken != \"\" {\n\t\tif !p.provider.ValidateSessionState(session) {\n\t\t\tlog.Printf(\"%s removing session. error validating %s\", remoteAddr, session)\n\t\t\tsaveSession = false\n\t\t\tsession = nil\n\t\t\tclearSession = true\n\t\t}\n\t}\n\n\tif session != nil && session.Email != \"\" && !p.Validator(session.Email) {\n\t\tlog.Printf(\"%s Permission Denied: removing session %s\", remoteAddr, session)\n\t\tsession = nil\n\t\tsaveSession = false\n\t\tclearSession = true\n\t}\n\n\tif saveSession && session != nil {\n\t\terr := p.SaveSession(rw, req, session)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"%s %s\", remoteAddr, err)\n\t\t\treturn http.StatusInternalServerError\n\t\t}\n\t}\n\n\tif clearSession {\n\t\tp.ClearSessionCookie(rw, req)\n\t}\n\n\tif session == nil {\n\t\tsession, err = p.CheckBasicAuth(req)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"%s %s\", remoteAddr, err)\n\t\t}\n\t}\n\n\tif session == nil {\n\t\treturn http.StatusForbidden\n\t}\n\n\t// At this point, the user is authenticated. proxy normally\n\tif p.PassBasicAuth {\n\t\treq.SetBasicAuth(session.User, p.BasicAuthPassword)\n\t\treq.Header[\"X-Forwarded-User\"] = []string{session.User}\n\t\tif session.Email != \"\" {\n\t\t\treq.Header[\"X-Forwarded-Email\"] = []string{session.Email}\n\t\t}\n\t}\n\tif p.PassUserHeaders {\n\t\treq.Header[\"X-Forwarded-User\"] = []string{session.User}\n\t\tif session.Email != \"\" {\n\t\t\treq.Header[\"X-Forwarded-Email\"] = []string{session.Email}\n\t\t}\n\t}\n\tif p.SetXAuthRequest {\n\t\trw.Header().Set(\"X-Auth-Request-User\", session.User)\n\t\tif session.Email != \"\" {\n\t\t\trw.Header().Set(\"X-Auth-Request-Email\", session.Email)\n\t\t}\n\t}\n\tif p.PassAccessToken && session.AccessToken != \"\" {\n\t\treq.Header[\"X-Forwarded-Access-Token\"] = []string{session.AccessToken}\n\t}\n\tif session.Email == \"\" {\n\t\trw.Header().Set(\"GAP-Auth\", session.User)\n\t} else {\n\t\trw.Header().Set(\"GAP-Auth\", session.Email)\n\t}\n\treturn http.StatusAccepted\n}\n\nfunc (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*providers.SessionState, error) {\n\tif p.HtpasswdFile == nil {\n\t\treturn nil, nil\n\t}\n\tauth := req.Header.Get(\"Authorization\")\n\tif auth == \"\" {\n\t\treturn nil, nil\n\t}\n\ts := strings.SplitN(auth, \" \", 2)\n\tif len(s) != 2 || s[0] != \"Basic\" {\n\t\treturn nil, fmt.Errorf(\"invalid Authorization header %s\", req.Header.Get(\"Authorization\"))\n\t}\n\tb, err := b64.StdEncoding.DecodeString(s[1])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpair := strings.SplitN(string(b), \":\", 2)\n\tif len(pair) != 2 {\n\t\treturn nil, fmt.Errorf(\"invalid format %s\", b)\n\t}\n\tif p.HtpasswdFile.Validate(pair[0], pair[1]) {\n\t\tlog.Printf(\"authenticated %q via basic auth\", pair[0])\n\t\treturn &providers.SessionState{User: pair[0]}, nil\n\t}\n\treturn nil, fmt.Errorf(\"%s not in HtpasswdFile\", pair[0])\n}\n"
  },
  {
    "path": "oauthproxy_test.go",
    "content": "package main\n\nimport (\n\t\"crypto\"\n\t\"encoding/base64\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bitly/oauth2_proxy/providers\"\n\t\"github.com/mbland/hmacauth\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc init() {\n\tlog.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)\n\n}\n\nfunc TestNewReverseProxy(t *testing.T) {\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(200)\n\t\thostname, _, _ := net.SplitHostPort(r.Host)\n\t\tw.Write([]byte(hostname))\n\t}))\n\tdefer backend.Close()\n\n\tbackendURL, _ := url.Parse(backend.URL)\n\tbackendHostname, backendPort, _ := net.SplitHostPort(backendURL.Host)\n\tbackendHost := net.JoinHostPort(backendHostname, backendPort)\n\tproxyURL, _ := url.Parse(backendURL.Scheme + \"://\" + backendHost + \"/\")\n\n\tproxyHandler := NewReverseProxy(proxyURL)\n\tsetProxyUpstreamHostHeader(proxyHandler, proxyURL)\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\n\tgetReq, _ := http.NewRequest(\"GET\", frontend.URL, nil)\n\tres, _ := http.DefaultClient.Do(getReq)\n\tbodyBytes, _ := ioutil.ReadAll(res.Body)\n\tif g, e := string(bodyBytes), backendHostname; g != e {\n\t\tt.Errorf(\"got body %q; expected %q\", g, e)\n\t}\n}\n\nfunc TestEncodedSlashes(t *testing.T) {\n\tvar seen string\n\tbackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(200)\n\t\tseen = r.RequestURI\n\t}))\n\tdefer backend.Close()\n\n\tb, _ := url.Parse(backend.URL)\n\tproxyHandler := NewReverseProxy(b)\n\tsetProxyDirector(proxyHandler)\n\tfrontend := httptest.NewServer(proxyHandler)\n\tdefer frontend.Close()\n\n\tf, _ := url.Parse(frontend.URL)\n\tencodedPath := \"/a%2Fb/?c=1\"\n\tgetReq := &http.Request{URL: &url.URL{Scheme: \"http\", Host: f.Host, Opaque: encodedPath}}\n\t_, err := http.DefaultClient.Do(getReq)\n\tif err != nil {\n\t\tt.Fatalf(\"err %s\", err)\n\t}\n\tif seen != encodedPath {\n\t\tt.Errorf(\"got bad request %q expected %q\", seen, encodedPath)\n\t}\n}\n\nfunc TestRobotsTxt(t *testing.T) {\n\topts := NewOptions()\n\topts.ClientID = \"bazquux\"\n\topts.ClientSecret = \"foobar\"\n\topts.CookieSecret = \"xyzzyplugh\"\n\topts.Validate()\n\n\tproxy := NewOAuthProxy(opts, func(string) bool { return true })\n\trw := httptest.NewRecorder()\n\treq, _ := http.NewRequest(\"GET\", \"/robots.txt\", nil)\n\tproxy.ServeHTTP(rw, req)\n\tassert.Equal(t, 200, rw.Code)\n\tassert.Equal(t, \"User-agent: *\\nDisallow: /\", rw.Body.String())\n}\n\ntype TestProvider struct {\n\t*providers.ProviderData\n\tEmailAddress string\n\tValidToken   bool\n}\n\nfunc NewTestProvider(provider_url *url.URL, email_address string) *TestProvider {\n\treturn &TestProvider{\n\t\tProviderData: &providers.ProviderData{\n\t\t\tProviderName: \"Test Provider\",\n\t\t\tLoginURL: &url.URL{\n\t\t\t\tScheme: \"http\",\n\t\t\t\tHost:   provider_url.Host,\n\t\t\t\tPath:   \"/oauth/authorize\",\n\t\t\t},\n\t\t\tRedeemURL: &url.URL{\n\t\t\t\tScheme: \"http\",\n\t\t\t\tHost:   provider_url.Host,\n\t\t\t\tPath:   \"/oauth/token\",\n\t\t\t},\n\t\t\tProfileURL: &url.URL{\n\t\t\t\tScheme: \"http\",\n\t\t\t\tHost:   provider_url.Host,\n\t\t\t\tPath:   \"/api/v1/profile\",\n\t\t\t},\n\t\t\tScope: \"profile.email\",\n\t\t},\n\t\tEmailAddress: email_address,\n\t}\n}\n\nfunc (tp *TestProvider) GetEmailAddress(session *providers.SessionState) (string, error) {\n\treturn tp.EmailAddress, nil\n}\n\nfunc (tp *TestProvider) ValidateSessionState(session *providers.SessionState) bool {\n\treturn tp.ValidToken\n}\n\nfunc TestBasicAuthPassword(t *testing.T) {\n\tprovider_server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tlog.Printf(\"%#v\", r)\n\t\turl := r.URL\n\t\tpayload := \"\"\n\t\tswitch url.Path {\n\t\tcase \"/oauth/token\":\n\t\t\tpayload = `{\"access_token\": \"my_auth_token\"}`\n\t\tdefault:\n\t\t\tpayload = r.Header.Get(\"Authorization\")\n\t\t\tif payload == \"\" {\n\t\t\t\tpayload = \"No Authorization header found.\"\n\t\t\t}\n\t\t}\n\t\tw.WriteHeader(200)\n\t\tw.Write([]byte(payload))\n\t}))\n\topts := NewOptions()\n\topts.Upstreams = append(opts.Upstreams, provider_server.URL)\n\t// The CookieSecret must be 32 bytes in order to create the AES\n\t// cipher.\n\topts.CookieSecret = \"xyzzyplughxyzzyplughxyzzyplughxp\"\n\topts.ClientID = \"bazquux\"\n\topts.ClientSecret = \"foobar\"\n\topts.CookieSecure = false\n\topts.PassBasicAuth = true\n\topts.PassUserHeaders = true\n\topts.BasicAuthPassword = \"This is a secure password\"\n\topts.Validate()\n\n\tprovider_url, _ := url.Parse(provider_server.URL)\n\tconst email_address = \"michael.bland@gsa.gov\"\n\tconst user_name = \"michael.bland\"\n\n\topts.provider = NewTestProvider(provider_url, email_address)\n\tproxy := NewOAuthProxy(opts, func(email string) bool {\n\t\treturn email == email_address\n\t})\n\n\trw := httptest.NewRecorder()\n\treq, _ := http.NewRequest(\"GET\", \"/oauth2/callback?code=callback_code&state=nonce:\",\n\t\tstrings.NewReader(\"\"))\n\treq.AddCookie(proxy.MakeCSRFCookie(req, \"nonce\", proxy.CookieExpire, time.Now()))\n\tproxy.ServeHTTP(rw, req)\n\tif rw.Code >= 400 {\n\t\tt.Fatalf(\"expected 3xx got %d\", rw.Code)\n\t}\n\tcookie := rw.HeaderMap[\"Set-Cookie\"][1]\n\n\tcookieName := proxy.CookieName\n\tvar value string\n\tkey_prefix := cookieName + \"=\"\n\n\tfor _, field := range strings.Split(cookie, \"; \") {\n\t\tvalue = strings.TrimPrefix(field, key_prefix)\n\t\tif value != field {\n\t\t\tbreak\n\t\t} else {\n\t\t\tvalue = \"\"\n\t\t}\n\t}\n\n\treq, _ = http.NewRequest(\"GET\", \"/\", strings.NewReader(\"\"))\n\treq.AddCookie(&http.Cookie{\n\t\tName:     cookieName,\n\t\tValue:    value,\n\t\tPath:     \"/\",\n\t\tExpires:  time.Now().Add(time.Duration(24)),\n\t\tHttpOnly: true,\n\t})\n\treq.AddCookie(proxy.MakeCSRFCookie(req, \"nonce\", proxy.CookieExpire, time.Now()))\n\n\trw = httptest.NewRecorder()\n\tproxy.ServeHTTP(rw, req)\n\n\texpectedHeader := \"Basic \" + base64.StdEncoding.EncodeToString([]byte(user_name+\":\"+opts.BasicAuthPassword))\n\tassert.Equal(t, expectedHeader, rw.Body.String())\n\tprovider_server.Close()\n}\n\ntype PassAccessTokenTest struct {\n\tprovider_server *httptest.Server\n\tproxy           *OAuthProxy\n\topts            *Options\n}\n\ntype PassAccessTokenTestOptions struct {\n\tPassAccessToken bool\n}\n\nfunc NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTest {\n\tt := &PassAccessTokenTest{}\n\n\tt.provider_server = httptest.NewServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tlog.Printf(\"%#v\", r)\n\t\t\turl := r.URL\n\t\t\tpayload := \"\"\n\t\t\tswitch url.Path {\n\t\t\tcase \"/oauth/token\":\n\t\t\t\tpayload = `{\"access_token\": \"my_auth_token\"}`\n\t\t\tdefault:\n\t\t\t\tpayload = r.Header.Get(\"X-Forwarded-Access-Token\")\n\t\t\t\tif payload == \"\" {\n\t\t\t\t\tpayload = \"No access token found.\"\n\t\t\t\t}\n\t\t\t}\n\t\t\tw.WriteHeader(200)\n\t\t\tw.Write([]byte(payload))\n\t\t}))\n\n\tt.opts = NewOptions()\n\tt.opts.Upstreams = append(t.opts.Upstreams, t.provider_server.URL)\n\t// The CookieSecret must be 32 bytes in order to create the AES\n\t// cipher.\n\tt.opts.CookieSecret = \"xyzzyplughxyzzyplughxyzzyplughxp\"\n\tt.opts.ClientID = \"bazquux\"\n\tt.opts.ClientSecret = \"foobar\"\n\tt.opts.CookieSecure = false\n\tt.opts.PassAccessToken = opts.PassAccessToken\n\tt.opts.Validate()\n\n\tprovider_url, _ := url.Parse(t.provider_server.URL)\n\tconst email_address = \"michael.bland@gsa.gov\"\n\n\tt.opts.provider = NewTestProvider(provider_url, email_address)\n\tt.proxy = NewOAuthProxy(t.opts, func(email string) bool {\n\t\treturn email == email_address\n\t})\n\treturn t\n}\n\nfunc (pat_test *PassAccessTokenTest) Close() {\n\tpat_test.provider_server.Close()\n}\n\nfunc (pat_test *PassAccessTokenTest) getCallbackEndpoint() (http_code int,\n\tcookie string) {\n\trw := httptest.NewRecorder()\n\treq, err := http.NewRequest(\"GET\", \"/oauth2/callback?code=callback_code&state=nonce:\",\n\t\tstrings.NewReader(\"\"))\n\tif err != nil {\n\t\treturn 0, \"\"\n\t}\n\treq.AddCookie(pat_test.proxy.MakeCSRFCookie(req, \"nonce\", time.Hour, time.Now()))\n\tpat_test.proxy.ServeHTTP(rw, req)\n\treturn rw.Code, rw.HeaderMap[\"Set-Cookie\"][1]\n}\n\nfunc (pat_test *PassAccessTokenTest) getRootEndpoint(cookie string) (http_code int, access_token string) {\n\tcookieName := pat_test.proxy.CookieName\n\tvar value string\n\tkey_prefix := cookieName + \"=\"\n\n\tfor _, field := range strings.Split(cookie, \"; \") {\n\t\tvalue = strings.TrimPrefix(field, key_prefix)\n\t\tif value != field {\n\t\t\tbreak\n\t\t} else {\n\t\t\tvalue = \"\"\n\t\t}\n\t}\n\tif value == \"\" {\n\t\treturn 0, \"\"\n\t}\n\n\treq, err := http.NewRequest(\"GET\", \"/\", strings.NewReader(\"\"))\n\tif err != nil {\n\t\treturn 0, \"\"\n\t}\n\treq.AddCookie(&http.Cookie{\n\t\tName:     cookieName,\n\t\tValue:    value,\n\t\tPath:     \"/\",\n\t\tExpires:  time.Now().Add(time.Duration(24)),\n\t\tHttpOnly: true,\n\t})\n\n\trw := httptest.NewRecorder()\n\tpat_test.proxy.ServeHTTP(rw, req)\n\treturn rw.Code, rw.Body.String()\n}\n\nfunc TestForwardAccessTokenUpstream(t *testing.T) {\n\tpat_test := NewPassAccessTokenTest(PassAccessTokenTestOptions{\n\t\tPassAccessToken: true,\n\t})\n\tdefer pat_test.Close()\n\n\t// A successful validation will redirect and set the auth cookie.\n\tcode, cookie := pat_test.getCallbackEndpoint()\n\tif code != 302 {\n\t\tt.Fatalf(\"expected 302; got %d\", code)\n\t}\n\tassert.NotEqual(t, nil, cookie)\n\n\t// Now we make a regular request; the access_token from the cookie is\n\t// forwarded as the \"X-Forwarded-Access-Token\" header. The token is\n\t// read by the test provider server and written in the response body.\n\tcode, payload := pat_test.getRootEndpoint(cookie)\n\tif code != 200 {\n\t\tt.Fatalf(\"expected 200; got %d\", code)\n\t}\n\tassert.Equal(t, \"my_auth_token\", payload)\n}\n\nfunc TestDoNotForwardAccessTokenUpstream(t *testing.T) {\n\tpat_test := NewPassAccessTokenTest(PassAccessTokenTestOptions{\n\t\tPassAccessToken: false,\n\t})\n\tdefer pat_test.Close()\n\n\t// A successful validation will redirect and set the auth cookie.\n\tcode, cookie := pat_test.getCallbackEndpoint()\n\tif code != 302 {\n\t\tt.Fatalf(\"expected 302; got %d\", code)\n\t}\n\tassert.NotEqual(t, nil, cookie)\n\n\t// Now we make a regular request, but the access token header should\n\t// not be present.\n\tcode, payload := pat_test.getRootEndpoint(cookie)\n\tif code != 200 {\n\t\tt.Fatalf(\"expected 200; got %d\", code)\n\t}\n\tassert.Equal(t, \"No access token found.\", payload)\n}\n\ntype SignInPageTest struct {\n\topts                    *Options\n\tproxy                   *OAuthProxy\n\tsign_in_regexp          *regexp.Regexp\n\tsign_in_provider_regexp *regexp.Regexp\n}\n\nconst signInRedirectPattern = `<input type=\"hidden\" name=\"rd\" value=\"(.*)\">`\nconst signInSkipProvider = `>Found<`\n\nfunc NewSignInPageTest(skipProvider bool) *SignInPageTest {\n\tvar sip_test SignInPageTest\n\n\tsip_test.opts = NewOptions()\n\tsip_test.opts.CookieSecret = \"foobar\"\n\tsip_test.opts.ClientID = \"bazquux\"\n\tsip_test.opts.ClientSecret = \"xyzzyplugh\"\n\tsip_test.opts.SkipProviderButton = skipProvider\n\tsip_test.opts.Validate()\n\n\tsip_test.proxy = NewOAuthProxy(sip_test.opts, func(email string) bool {\n\t\treturn true\n\t})\n\tsip_test.sign_in_regexp = regexp.MustCompile(signInRedirectPattern)\n\tsip_test.sign_in_provider_regexp = regexp.MustCompile(signInSkipProvider)\n\n\treturn &sip_test\n}\n\nfunc (sip_test *SignInPageTest) GetEndpoint(endpoint string) (int, string) {\n\trw := httptest.NewRecorder()\n\treq, _ := http.NewRequest(\"GET\", endpoint, strings.NewReader(\"\"))\n\tsip_test.proxy.ServeHTTP(rw, req)\n\treturn rw.Code, rw.Body.String()\n}\n\nfunc TestSignInPageIncludesTargetRedirect(t *testing.T) {\n\tsip_test := NewSignInPageTest(false)\n\tconst endpoint = \"/some/random/endpoint\"\n\n\tcode, body := sip_test.GetEndpoint(endpoint)\n\tassert.Equal(t, 403, code)\n\n\tmatch := sip_test.sign_in_regexp.FindStringSubmatch(body)\n\tif match == nil {\n\t\tt.Fatal(\"Did not find pattern in body: \" +\n\t\t\tsignInRedirectPattern + \"\\nBody:\\n\" + body)\n\t}\n\tif match[1] != endpoint {\n\t\tt.Fatal(`expected redirect to \"` + endpoint +\n\t\t\t`\", but was \"` + match[1] + `\"`)\n\t}\n}\n\nfunc TestSignInPageDirectAccessRedirectsToRoot(t *testing.T) {\n\tsip_test := NewSignInPageTest(false)\n\tcode, body := sip_test.GetEndpoint(\"/oauth2/sign_in\")\n\tassert.Equal(t, 200, code)\n\n\tmatch := sip_test.sign_in_regexp.FindStringSubmatch(body)\n\tif match == nil {\n\t\tt.Fatal(\"Did not find pattern in body: \" +\n\t\t\tsignInRedirectPattern + \"\\nBody:\\n\" + body)\n\t}\n\tif match[1] != \"/\" {\n\t\tt.Fatal(`expected redirect to \"/\", but was \"` + match[1] + `\"`)\n\t}\n}\n\nfunc TestSignInPageSkipProvider(t *testing.T) {\n\tsip_test := NewSignInPageTest(true)\n\tconst endpoint = \"/some/random/endpoint\"\n\n\tcode, body := sip_test.GetEndpoint(endpoint)\n\tassert.Equal(t, 302, code)\n\n\tmatch := sip_test.sign_in_provider_regexp.FindStringSubmatch(body)\n\tif match == nil {\n\t\tt.Fatal(\"Did not find pattern in body: \" +\n\t\t\tsignInSkipProvider + \"\\nBody:\\n\" + body)\n\t}\n}\n\nfunc TestSignInPageSkipProviderDirect(t *testing.T) {\n\tsip_test := NewSignInPageTest(true)\n\tconst endpoint = \"/sign_in\"\n\n\tcode, body := sip_test.GetEndpoint(endpoint)\n\tassert.Equal(t, 302, code)\n\n\tmatch := sip_test.sign_in_provider_regexp.FindStringSubmatch(body)\n\tif match == nil {\n\t\tt.Fatal(\"Did not find pattern in body: \" +\n\t\t\tsignInSkipProvider + \"\\nBody:\\n\" + body)\n\t}\n}\n\ntype ProcessCookieTest struct {\n\topts          *Options\n\tproxy         *OAuthProxy\n\trw            *httptest.ResponseRecorder\n\treq           *http.Request\n\tprovider      TestProvider\n\tresponse_code int\n\tvalidate_user bool\n}\n\ntype ProcessCookieTestOpts struct {\n\tprovider_validate_cookie_response bool\n}\n\nfunc NewProcessCookieTest(opts ProcessCookieTestOpts) *ProcessCookieTest {\n\tvar pc_test ProcessCookieTest\n\n\tpc_test.opts = NewOptions()\n\tpc_test.opts.ClientID = \"bazquux\"\n\tpc_test.opts.ClientSecret = \"xyzzyplugh\"\n\tpc_test.opts.CookieSecret = \"0123456789abcdefabcd\"\n\t// First, set the CookieRefresh option so proxy.AesCipher is created,\n\t// needed to encrypt the access_token.\n\tpc_test.opts.CookieRefresh = time.Hour\n\tpc_test.opts.Validate()\n\n\tpc_test.proxy = NewOAuthProxy(pc_test.opts, func(email string) bool {\n\t\treturn pc_test.validate_user\n\t})\n\tpc_test.proxy.provider = &TestProvider{\n\t\tValidToken: opts.provider_validate_cookie_response,\n\t}\n\n\t// Now, zero-out proxy.CookieRefresh for the cases that don't involve\n\t// access_token validation.\n\tpc_test.proxy.CookieRefresh = time.Duration(0)\n\tpc_test.rw = httptest.NewRecorder()\n\tpc_test.req, _ = http.NewRequest(\"GET\", \"/\", strings.NewReader(\"\"))\n\tpc_test.validate_user = true\n\treturn &pc_test\n}\n\nfunc NewProcessCookieTestWithDefaults() *ProcessCookieTest {\n\treturn NewProcessCookieTest(ProcessCookieTestOpts{\n\t\tprovider_validate_cookie_response: true,\n\t})\n}\n\nfunc (p *ProcessCookieTest) MakeCookie(value string, ref time.Time) *http.Cookie {\n\treturn p.proxy.MakeSessionCookie(p.req, value, p.opts.CookieExpire, ref)\n}\n\nfunc (p *ProcessCookieTest) SaveSession(s *providers.SessionState, ref time.Time) error {\n\tvalue, err := p.proxy.provider.CookieForSession(s, p.proxy.CookieCipher)\n\tif err != nil {\n\t\treturn err\n\t}\n\tp.req.AddCookie(p.proxy.MakeSessionCookie(p.req, value, p.proxy.CookieExpire, ref))\n\treturn nil\n}\n\nfunc (p *ProcessCookieTest) LoadCookiedSession() (*providers.SessionState, time.Duration, error) {\n\treturn p.proxy.LoadCookiedSession(p.req)\n}\n\nfunc TestLoadCookiedSession(t *testing.T) {\n\tpc_test := NewProcessCookieTestWithDefaults()\n\n\tstartSession := &providers.SessionState{Email: \"michael.bland@gsa.gov\", AccessToken: \"my_access_token\"}\n\tpc_test.SaveSession(startSession, time.Now())\n\n\tsession, _, err := pc_test.LoadCookiedSession()\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, startSession.Email, session.Email)\n\tassert.Equal(t, \"michael.bland\", session.User)\n\tassert.Equal(t, startSession.AccessToken, session.AccessToken)\n}\n\nfunc TestProcessCookieNoCookieError(t *testing.T) {\n\tpc_test := NewProcessCookieTestWithDefaults()\n\n\tsession, _, err := pc_test.LoadCookiedSession()\n\tassert.Equal(t, \"Cookie \\\"_oauth2_proxy\\\" not present\", err.Error())\n\tif session != nil {\n\t\tt.Errorf(\"expected nil session. got %#v\", session)\n\t}\n}\n\nfunc TestProcessCookieRefreshNotSet(t *testing.T) {\n\tpc_test := NewProcessCookieTestWithDefaults()\n\tpc_test.proxy.CookieExpire = time.Duration(23) * time.Hour\n\treference := time.Now().Add(time.Duration(-2) * time.Hour)\n\n\tstartSession := &providers.SessionState{Email: \"michael.bland@gsa.gov\", AccessToken: \"my_access_token\"}\n\tpc_test.SaveSession(startSession, reference)\n\n\tsession, age, err := pc_test.LoadCookiedSession()\n\tassert.Equal(t, nil, err)\n\tif age < time.Duration(-2)*time.Hour {\n\t\tt.Errorf(\"cookie too young %v\", age)\n\t}\n\tassert.Equal(t, startSession.Email, session.Email)\n}\n\nfunc TestProcessCookieFailIfCookieExpired(t *testing.T) {\n\tpc_test := NewProcessCookieTestWithDefaults()\n\tpc_test.proxy.CookieExpire = time.Duration(24) * time.Hour\n\treference := time.Now().Add(time.Duration(25) * time.Hour * -1)\n\tstartSession := &providers.SessionState{Email: \"michael.bland@gsa.gov\", AccessToken: \"my_access_token\"}\n\tpc_test.SaveSession(startSession, reference)\n\n\tsession, _, err := pc_test.LoadCookiedSession()\n\tassert.NotEqual(t, nil, err)\n\tif session != nil {\n\t\tt.Errorf(\"expected nil session %#v\", session)\n\t}\n}\n\nfunc TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {\n\tpc_test := NewProcessCookieTestWithDefaults()\n\tpc_test.proxy.CookieExpire = time.Duration(24) * time.Hour\n\treference := time.Now().Add(time.Duration(25) * time.Hour * -1)\n\tstartSession := &providers.SessionState{Email: \"michael.bland@gsa.gov\", AccessToken: \"my_access_token\"}\n\tpc_test.SaveSession(startSession, reference)\n\n\tpc_test.proxy.CookieRefresh = time.Hour\n\tsession, _, err := pc_test.LoadCookiedSession()\n\tassert.NotEqual(t, nil, err)\n\tif session != nil {\n\t\tt.Errorf(\"expected nil session %#v\", session)\n\t}\n}\n\nfunc NewAuthOnlyEndpointTest() *ProcessCookieTest {\n\tpc_test := NewProcessCookieTestWithDefaults()\n\tpc_test.req, _ = http.NewRequest(\"GET\",\n\t\tpc_test.opts.ProxyPrefix+\"/auth\", nil)\n\treturn pc_test\n}\n\nfunc TestAuthOnlyEndpointAccepted(t *testing.T) {\n\ttest := NewAuthOnlyEndpointTest()\n\tstartSession := &providers.SessionState{\n\t\tEmail: \"michael.bland@gsa.gov\", AccessToken: \"my_access_token\"}\n\ttest.SaveSession(startSession, time.Now())\n\n\ttest.proxy.ServeHTTP(test.rw, test.req)\n\tassert.Equal(t, http.StatusAccepted, test.rw.Code)\n\tbodyBytes, _ := ioutil.ReadAll(test.rw.Body)\n\tassert.Equal(t, \"\", string(bodyBytes))\n}\n\nfunc TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {\n\ttest := NewAuthOnlyEndpointTest()\n\n\ttest.proxy.ServeHTTP(test.rw, test.req)\n\tassert.Equal(t, http.StatusUnauthorized, test.rw.Code)\n\tbodyBytes, _ := ioutil.ReadAll(test.rw.Body)\n\tassert.Equal(t, \"unauthorized request\\n\", string(bodyBytes))\n}\n\nfunc TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) {\n\ttest := NewAuthOnlyEndpointTest()\n\ttest.proxy.CookieExpire = time.Duration(24) * time.Hour\n\treference := time.Now().Add(time.Duration(25) * time.Hour * -1)\n\tstartSession := &providers.SessionState{\n\t\tEmail: \"michael.bland@gsa.gov\", AccessToken: \"my_access_token\"}\n\ttest.SaveSession(startSession, reference)\n\n\ttest.proxy.ServeHTTP(test.rw, test.req)\n\tassert.Equal(t, http.StatusUnauthorized, test.rw.Code)\n\tbodyBytes, _ := ioutil.ReadAll(test.rw.Body)\n\tassert.Equal(t, \"unauthorized request\\n\", string(bodyBytes))\n}\n\nfunc TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {\n\ttest := NewAuthOnlyEndpointTest()\n\tstartSession := &providers.SessionState{\n\t\tEmail: \"michael.bland@gsa.gov\", AccessToken: \"my_access_token\"}\n\ttest.SaveSession(startSession, time.Now())\n\ttest.validate_user = false\n\n\ttest.proxy.ServeHTTP(test.rw, test.req)\n\tassert.Equal(t, http.StatusUnauthorized, test.rw.Code)\n\tbodyBytes, _ := ioutil.ReadAll(test.rw.Body)\n\tassert.Equal(t, \"unauthorized request\\n\", string(bodyBytes))\n}\n\nfunc TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) {\n\tvar pc_test ProcessCookieTest\n\n\tpc_test.opts = NewOptions()\n\tpc_test.opts.SetXAuthRequest = true\n\tpc_test.opts.Validate()\n\n\tpc_test.proxy = NewOAuthProxy(pc_test.opts, func(email string) bool {\n\t\treturn pc_test.validate_user\n\t})\n\tpc_test.proxy.provider = &TestProvider{\n\t\tValidToken: true,\n\t}\n\n\tpc_test.validate_user = true\n\n\tpc_test.rw = httptest.NewRecorder()\n\tpc_test.req, _ = http.NewRequest(\"GET\",\n\t\tpc_test.opts.ProxyPrefix+\"/auth\", nil)\n\n\tstartSession := &providers.SessionState{\n\t\tUser: \"oauth_user\", Email: \"oauth_user@example.com\", AccessToken: \"oauth_token\"}\n\tpc_test.SaveSession(startSession, time.Now())\n\n\tpc_test.proxy.ServeHTTP(pc_test.rw, pc_test.req)\n\tassert.Equal(t, http.StatusAccepted, pc_test.rw.Code)\n\tassert.Equal(t, \"oauth_user\", pc_test.rw.HeaderMap[\"X-Auth-Request-User\"][0])\n\tassert.Equal(t, \"oauth_user@example.com\", pc_test.rw.HeaderMap[\"X-Auth-Request-Email\"][0])\n}\n\nfunc TestAuthSkippedForPreflightRequests(t *testing.T) {\n\tupstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(200)\n\t\tw.Write([]byte(\"response\"))\n\t}))\n\tdefer upstream.Close()\n\n\topts := NewOptions()\n\topts.Upstreams = append(opts.Upstreams, upstream.URL)\n\topts.ClientID = \"bazquux\"\n\topts.ClientSecret = \"foobar\"\n\topts.CookieSecret = \"xyzzyplugh\"\n\topts.SkipAuthPreflight = true\n\topts.Validate()\n\n\tupstream_url, _ := url.Parse(upstream.URL)\n\topts.provider = NewTestProvider(upstream_url, \"\")\n\n\tproxy := NewOAuthProxy(opts, func(string) bool { return false })\n\trw := httptest.NewRecorder()\n\treq, _ := http.NewRequest(\"OPTIONS\", \"/preflight-request\", nil)\n\tproxy.ServeHTTP(rw, req)\n\n\tassert.Equal(t, 200, rw.Code)\n\tassert.Equal(t, \"response\", rw.Body.String())\n}\n\ntype SignatureAuthenticator struct {\n\tauth hmacauth.HmacAuth\n}\n\nfunc (v *SignatureAuthenticator) Authenticate(w http.ResponseWriter, r *http.Request) {\n\tresult, headerSig, computedSig := v.auth.AuthenticateRequest(r)\n\tif result == hmacauth.ResultNoSignature {\n\t\tw.Write([]byte(\"no signature received\"))\n\t} else if result == hmacauth.ResultMatch {\n\t\tw.Write([]byte(\"signatures match\"))\n\t} else if result == hmacauth.ResultMismatch {\n\t\tw.Write([]byte(\"signatures do not match:\" +\n\t\t\t\"\\n  received: \" + headerSig +\n\t\t\t\"\\n  computed: \" + computedSig))\n\t} else {\n\t\tpanic(\"Unknown result value: \" + result.String())\n\t}\n}\n\ntype SignatureTest struct {\n\topts          *Options\n\tupstream      *httptest.Server\n\tupstream_host string\n\tprovider      *httptest.Server\n\theader        http.Header\n\trw            *httptest.ResponseRecorder\n\tauthenticator *SignatureAuthenticator\n}\n\nfunc NewSignatureTest() *SignatureTest {\n\topts := NewOptions()\n\topts.CookieSecret = \"cookie secret\"\n\topts.ClientID = \"client ID\"\n\topts.ClientSecret = \"client secret\"\n\topts.EmailDomains = []string{\"acm.org\"}\n\n\tauthenticator := &SignatureAuthenticator{}\n\tupstream := httptest.NewServer(\n\t\thttp.HandlerFunc(authenticator.Authenticate))\n\tupstream_url, _ := url.Parse(upstream.URL)\n\topts.Upstreams = append(opts.Upstreams, upstream.URL)\n\n\tproviderHandler := func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(`{\"access_token\": \"my_auth_token\"}`))\n\t}\n\tprovider := httptest.NewServer(http.HandlerFunc(providerHandler))\n\tprovider_url, _ := url.Parse(provider.URL)\n\topts.provider = NewTestProvider(provider_url, \"mbland@acm.org\")\n\n\treturn &SignatureTest{\n\t\topts,\n\t\tupstream,\n\t\tupstream_url.Host,\n\t\tprovider,\n\t\tmake(http.Header),\n\t\thttptest.NewRecorder(),\n\t\tauthenticator,\n\t}\n}\n\nfunc (st *SignatureTest) Close() {\n\tst.provider.Close()\n\tst.upstream.Close()\n}\n\n// fakeNetConn simulates an http.Request.Body buffer that will be consumed\n// when it is read by the hmacauth.HmacAuth if not handled properly. See:\n//   https://github.com/18F/hmacauth/pull/4\ntype fakeNetConn struct {\n\treqBody string\n}\n\nfunc (fnc *fakeNetConn) Read(p []byte) (n int, err error) {\n\tif bodyLen := len(fnc.reqBody); bodyLen != 0 {\n\t\tcopy(p, fnc.reqBody)\n\t\tfnc.reqBody = \"\"\n\t\treturn bodyLen, io.EOF\n\t}\n\treturn 0, io.EOF\n}\n\nfunc (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) {\n\terr := st.opts.Validate()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tproxy := NewOAuthProxy(st.opts, func(email string) bool { return true })\n\n\tvar bodyBuf io.ReadCloser\n\tif body != \"\" {\n\t\tbodyBuf = ioutil.NopCloser(&fakeNetConn{reqBody: body})\n\t}\n\treq := httptest.NewRequest(method, \"/foo/bar\", bodyBuf)\n\treq.Header = st.header\n\n\tstate := &providers.SessionState{\n\t\tEmail: \"mbland@acm.org\", AccessToken: \"my_access_token\"}\n\tvalue, err := proxy.provider.CookieForSession(state, proxy.CookieCipher)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcookie := proxy.MakeSessionCookie(req, value, proxy.CookieExpire, time.Now())\n\treq.AddCookie(cookie)\n\t// This is used by the upstream to validate the signature.\n\tst.authenticator.auth = hmacauth.NewHmacAuth(\n\t\tcrypto.SHA1, []byte(key), SignatureHeader, SignatureHeaders)\n\tproxy.ServeHTTP(st.rw, req)\n}\n\nfunc TestNoRequestSignature(t *testing.T) {\n\tst := NewSignatureTest()\n\tdefer st.Close()\n\tst.MakeRequestWithExpectedKey(\"GET\", \"\", \"\")\n\tassert.Equal(t, 200, st.rw.Code)\n\tassert.Equal(t, st.rw.Body.String(), \"no signature received\")\n}\n\nfunc TestRequestSignatureGetRequest(t *testing.T) {\n\tst := NewSignatureTest()\n\tdefer st.Close()\n\tst.opts.SignatureKey = \"sha1:foobar\"\n\tst.MakeRequestWithExpectedKey(\"GET\", \"\", \"foobar\")\n\tassert.Equal(t, 200, st.rw.Code)\n\tassert.Equal(t, st.rw.Body.String(), \"signatures match\")\n}\n\nfunc TestRequestSignaturePostRequest(t *testing.T) {\n\tst := NewSignatureTest()\n\tdefer st.Close()\n\tst.opts.SignatureKey = \"sha1:foobar\"\n\tpayload := `{ \"hello\": \"world!\" }`\n\tst.MakeRequestWithExpectedKey(\"POST\", payload, \"foobar\")\n\tassert.Equal(t, 200, st.rw.Code)\n\tassert.Equal(t, st.rw.Body.String(), \"signatures match\")\n}\n"
  },
  {
    "path": "options.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bitly/oauth2_proxy/providers\"\n\toidc \"github.com/coreos/go-oidc\"\n\t\"github.com/mbland/hmacauth\"\n)\n\n// Configuration Options that can be set by Command Line Flag, or Config File\ntype Options struct {\n\tProxyPrefix  string `flag:\"proxy-prefix\" cfg:\"proxy-prefix\"`\n\tHttpAddress  string `flag:\"http-address\" cfg:\"http_address\"`\n\tHttpsAddress string `flag:\"https-address\" cfg:\"https_address\"`\n\tRedirectURL  string `flag:\"redirect-url\" cfg:\"redirect_url\"`\n\tClientID     string `flag:\"client-id\" cfg:\"client_id\" env:\"OAUTH2_PROXY_CLIENT_ID\"`\n\tClientSecret string `flag:\"client-secret\" cfg:\"client_secret\" env:\"OAUTH2_PROXY_CLIENT_SECRET\"`\n\tTLSCertFile  string `flag:\"tls-cert\" cfg:\"tls_cert_file\"`\n\tTLSKeyFile   string `flag:\"tls-key\" cfg:\"tls_key_file\"`\n\n\tAuthenticatedEmailsFile  string   `flag:\"authenticated-emails-file\" cfg:\"authenticated_emails_file\"`\n\tAzureTenant              string   `flag:\"azure-tenant\" cfg:\"azure_tenant\"`\n\tEmailDomains             []string `flag:\"email-domain\" cfg:\"email_domains\"`\n\tGitHubOrg                string   `flag:\"github-org\" cfg:\"github_org\"`\n\tGitHubTeam               string   `flag:\"github-team\" cfg:\"github_team\"`\n\tGoogleGroups             []string `flag:\"google-group\" cfg:\"google_group\"`\n\tGoogleAdminEmail         string   `flag:\"google-admin-email\" cfg:\"google_admin_email\"`\n\tGoogleServiceAccountJSON string   `flag:\"google-service-account-json\" cfg:\"google_service_account_json\"`\n\tHtpasswdFile             string   `flag:\"htpasswd-file\" cfg:\"htpasswd_file\"`\n\tDisplayHtpasswdForm      bool     `flag:\"display-htpasswd-form\" cfg:\"display_htpasswd_form\"`\n\tCustomTemplatesDir       string   `flag:\"custom-templates-dir\" cfg:\"custom_templates_dir\"`\n\tFooter                   string   `flag:\"footer\" cfg:\"footer\"`\n\n\tCookieName     string        `flag:\"cookie-name\" cfg:\"cookie_name\" env:\"OAUTH2_PROXY_COOKIE_NAME\"`\n\tCookieSecret   string        `flag:\"cookie-secret\" cfg:\"cookie_secret\" env:\"OAUTH2_PROXY_COOKIE_SECRET\"`\n\tCookieDomain   string        `flag:\"cookie-domain\" cfg:\"cookie_domain\" env:\"OAUTH2_PROXY_COOKIE_DOMAIN\"`\n\tCookieExpire   time.Duration `flag:\"cookie-expire\" cfg:\"cookie_expire\" env:\"OAUTH2_PROXY_COOKIE_EXPIRE\"`\n\tCookieRefresh  time.Duration `flag:\"cookie-refresh\" cfg:\"cookie_refresh\" env:\"OAUTH2_PROXY_COOKIE_REFRESH\"`\n\tCookieSecure   bool          `flag:\"cookie-secure\" cfg:\"cookie_secure\"`\n\tCookieHttpOnly bool          `flag:\"cookie-httponly\" cfg:\"cookie_httponly\"`\n\n\tUpstreams             []string `flag:\"upstream\" cfg:\"upstreams\"`\n\tSkipAuthRegex         []string `flag:\"skip-auth-regex\" cfg:\"skip_auth_regex\"`\n\tPassBasicAuth         bool     `flag:\"pass-basic-auth\" cfg:\"pass_basic_auth\"`\n\tBasicAuthPassword     string   `flag:\"basic-auth-password\" cfg:\"basic_auth_password\"`\n\tPassAccessToken       bool     `flag:\"pass-access-token\" cfg:\"pass_access_token\"`\n\tPassHostHeader        bool     `flag:\"pass-host-header\" cfg:\"pass_host_header\"`\n\tSkipProviderButton    bool     `flag:\"skip-provider-button\" cfg:\"skip_provider_button\"`\n\tPassUserHeaders       bool     `flag:\"pass-user-headers\" cfg:\"pass_user_headers\"`\n\tSSLInsecureSkipVerify bool     `flag:\"ssl-insecure-skip-verify\" cfg:\"ssl_insecure_skip_verify\"`\n\tSetXAuthRequest       bool     `flag:\"set-xauthrequest\" cfg:\"set_xauthrequest\"`\n\tSkipAuthPreflight     bool     `flag:\"skip-auth-preflight\" cfg:\"skip_auth_preflight\"`\n\n\t// These options allow for other providers besides Google, with\n\t// potential overrides.\n\tProvider          string `flag:\"provider\" cfg:\"provider\"`\n\tOIDCIssuerURL     string `flag:\"oidc-issuer-url\" cfg:\"oidc_issuer_url\"`\n\tLoginURL          string `flag:\"login-url\" cfg:\"login_url\"`\n\tRedeemURL         string `flag:\"redeem-url\" cfg:\"redeem_url\"`\n\tProfileURL        string `flag:\"profile-url\" cfg:\"profile_url\"`\n\tProtectedResource string `flag:\"resource\" cfg:\"resource\"`\n\tValidateURL       string `flag:\"validate-url\" cfg:\"validate_url\"`\n\tScope             string `flag:\"scope\" cfg:\"scope\"`\n\tApprovalPrompt    string `flag:\"approval-prompt\" cfg:\"approval_prompt\"`\n\n\tRequestLogging       bool   `flag:\"request-logging\" cfg:\"request_logging\"`\n\tRequestLoggingFormat string `flag:\"request-logging-format\" cfg:\"request_logging_format\"`\n\n\tSignatureKey string `flag:\"signature-key\" cfg:\"signature_key\" env:\"OAUTH2_PROXY_SIGNATURE_KEY\"`\n\n\t// internal values that are set after config validation\n\tredirectURL   *url.URL\n\tproxyURLs     []*url.URL\n\tCompiledRegex []*regexp.Regexp\n\tprovider      providers.Provider\n\tsignatureData *SignatureData\n\toidcVerifier  *oidc.IDTokenVerifier\n}\n\ntype SignatureData struct {\n\thash crypto.Hash\n\tkey  string\n}\n\nfunc NewOptions() *Options {\n\treturn &Options{\n\t\tProxyPrefix:          \"/oauth2\",\n\t\tHttpAddress:          \"127.0.0.1:4180\",\n\t\tHttpsAddress:         \":443\",\n\t\tDisplayHtpasswdForm:  true,\n\t\tCookieName:           \"_oauth2_proxy\",\n\t\tCookieSecure:         true,\n\t\tCookieHttpOnly:       true,\n\t\tCookieExpire:         time.Duration(168) * time.Hour,\n\t\tCookieRefresh:        time.Duration(0),\n\t\tSetXAuthRequest:      false,\n\t\tSkipAuthPreflight:    false,\n\t\tPassBasicAuth:        true,\n\t\tPassUserHeaders:      true,\n\t\tPassAccessToken:      false,\n\t\tPassHostHeader:       true,\n\t\tApprovalPrompt:       \"force\",\n\t\tRequestLogging:       true,\n\t\tRequestLoggingFormat: defaultRequestLoggingFormat,\n\t}\n}\n\nfunc parseURL(to_parse string, urltype string, msgs []string) (*url.URL, []string) {\n\tparsed, err := url.Parse(to_parse)\n\tif err != nil {\n\t\treturn nil, append(msgs, fmt.Sprintf(\n\t\t\t\"error parsing %s-url=%q %s\", urltype, to_parse, err))\n\t}\n\treturn parsed, msgs\n}\n\nfunc (o *Options) Validate() error {\n\tif o.SSLInsecureSkipVerify {\n\t\t// TODO: Accept a certificate bundle.\n\t\tinsecureTransport := &http.Transport{\n\t\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t}\n\t\thttp.DefaultClient = &http.Client{Transport: insecureTransport}\n\t}\n\n\tmsgs := make([]string, 0)\n\tif o.CookieSecret == \"\" {\n\t\tmsgs = append(msgs, \"missing setting: cookie-secret\")\n\t}\n\tif o.ClientID == \"\" {\n\t\tmsgs = append(msgs, \"missing setting: client-id\")\n\t}\n\tif o.ClientSecret == \"\" {\n\t\tmsgs = append(msgs, \"missing setting: client-secret\")\n\t}\n\tif o.AuthenticatedEmailsFile == \"\" && len(o.EmailDomains) == 0 && o.HtpasswdFile == \"\" {\n\t\tmsgs = append(msgs, \"missing setting for email validation: email-domain or authenticated-emails-file required.\"+\n\t\t\t\"\\n      use email-domain=* to authorize all email addresses\")\n\t}\n\n\tif o.OIDCIssuerURL != \"\" {\n\t\t// Configure discoverable provider data.\n\t\tprovider, err := oidc.NewProvider(context.Background(), o.OIDCIssuerURL)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\to.oidcVerifier = provider.Verifier(&oidc.Config{\n\t\t\tClientID: o.ClientID,\n\t\t})\n\t\to.LoginURL = provider.Endpoint().AuthURL\n\t\to.RedeemURL = provider.Endpoint().TokenURL\n\t\tif o.Scope == \"\" {\n\t\t\to.Scope = \"openid email profile\"\n\t\t}\n\t}\n\n\to.redirectURL, msgs = parseURL(o.RedirectURL, \"redirect\", msgs)\n\n\tfor _, u := range o.Upstreams {\n\t\tupstreamURL, err := url.Parse(u)\n\t\tif err != nil {\n\t\t\tmsgs = append(msgs, fmt.Sprintf(\"error parsing upstream: %s\", err))\n\t\t} else {\n\t\t\tif upstreamURL.Path == \"\" {\n\t\t\t\tupstreamURL.Path = \"/\"\n\t\t\t}\n\t\t\to.proxyURLs = append(o.proxyURLs, upstreamURL)\n\t\t}\n\t}\n\n\tfor _, u := range o.SkipAuthRegex {\n\t\tCompiledRegex, err := regexp.Compile(u)\n\t\tif err != nil {\n\t\t\tmsgs = append(msgs, fmt.Sprintf(\"error compiling regex=%q %s\", u, err))\n\t\t\tcontinue\n\t\t}\n\t\to.CompiledRegex = append(o.CompiledRegex, CompiledRegex)\n\t}\n\tmsgs = parseProviderInfo(o, msgs)\n\n\tif o.PassAccessToken || (o.CookieRefresh != time.Duration(0)) {\n\t\tvalid_cookie_secret_size := false\n\t\tfor _, i := range []int{16, 24, 32} {\n\t\t\tif len(secretBytes(o.CookieSecret)) == i {\n\t\t\t\tvalid_cookie_secret_size = true\n\t\t\t}\n\t\t}\n\t\tvar decoded bool\n\t\tif string(secretBytes(o.CookieSecret)) != o.CookieSecret {\n\t\t\tdecoded = true\n\t\t}\n\t\tif valid_cookie_secret_size == false {\n\t\t\tvar suffix string\n\t\t\tif decoded {\n\t\t\t\tsuffix = fmt.Sprintf(\" note: cookie secret was base64 decoded from %q\", o.CookieSecret)\n\t\t\t}\n\t\t\tmsgs = append(msgs, fmt.Sprintf(\n\t\t\t\t\"cookie_secret must be 16, 24, or 32 bytes \"+\n\t\t\t\t\t\"to create an AES cipher when \"+\n\t\t\t\t\t\"pass_access_token == true or \"+\n\t\t\t\t\t\"cookie_refresh != 0, but is %d bytes.%s\",\n\t\t\t\tlen(secretBytes(o.CookieSecret)), suffix))\n\t\t}\n\t}\n\n\tif o.CookieRefresh >= o.CookieExpire {\n\t\tmsgs = append(msgs, fmt.Sprintf(\n\t\t\t\"cookie_refresh (%s) must be less than \"+\n\t\t\t\t\"cookie_expire (%s)\",\n\t\t\to.CookieRefresh.String(),\n\t\t\to.CookieExpire.String()))\n\t}\n\n\tif len(o.GoogleGroups) > 0 || o.GoogleAdminEmail != \"\" || o.GoogleServiceAccountJSON != \"\" {\n\t\tif len(o.GoogleGroups) < 1 {\n\t\t\tmsgs = append(msgs, \"missing setting: google-group\")\n\t\t}\n\t\tif o.GoogleAdminEmail == \"\" {\n\t\t\tmsgs = append(msgs, \"missing setting: google-admin-email\")\n\t\t}\n\t\tif o.GoogleServiceAccountJSON == \"\" {\n\t\t\tmsgs = append(msgs, \"missing setting: google-service-account-json\")\n\t\t}\n\t}\n\n\tmsgs = parseSignatureKey(o, msgs)\n\tmsgs = validateCookieName(o, msgs)\n\n\tif len(msgs) != 0 {\n\t\treturn fmt.Errorf(\"Invalid configuration:\\n  %s\",\n\t\t\tstrings.Join(msgs, \"\\n  \"))\n\t}\n\treturn nil\n}\n\nfunc parseProviderInfo(o *Options, msgs []string) []string {\n\tp := &providers.ProviderData{\n\t\tScope:          o.Scope,\n\t\tClientID:       o.ClientID,\n\t\tClientSecret:   o.ClientSecret,\n\t\tApprovalPrompt: o.ApprovalPrompt,\n\t}\n\tp.LoginURL, msgs = parseURL(o.LoginURL, \"login\", msgs)\n\tp.RedeemURL, msgs = parseURL(o.RedeemURL, \"redeem\", msgs)\n\tp.ProfileURL, msgs = parseURL(o.ProfileURL, \"profile\", msgs)\n\tp.ValidateURL, msgs = parseURL(o.ValidateURL, \"validate\", msgs)\n\tp.ProtectedResource, msgs = parseURL(o.ProtectedResource, \"resource\", msgs)\n\n\to.provider = providers.New(o.Provider, p)\n\tswitch p := o.provider.(type) {\n\tcase *providers.AzureProvider:\n\t\tp.Configure(o.AzureTenant)\n\tcase *providers.GitHubProvider:\n\t\tp.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)\n\tcase *providers.GoogleProvider:\n\t\tif o.GoogleServiceAccountJSON != \"\" {\n\t\t\tfile, err := os.Open(o.GoogleServiceAccountJSON)\n\t\t\tif err != nil {\n\t\t\t\tmsgs = append(msgs, \"invalid Google credentials file: \"+o.GoogleServiceAccountJSON)\n\t\t\t} else {\n\t\t\t\tp.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file)\n\t\t\t}\n\t\t}\n\tcase *providers.OIDCProvider:\n\t\tif o.oidcVerifier == nil {\n\t\t\tmsgs = append(msgs, \"oidc provider requires an oidc issuer URL\")\n\t\t} else {\n\t\t\tp.Verifier = o.oidcVerifier\n\t\t}\n\t}\n\treturn msgs\n}\n\nfunc parseSignatureKey(o *Options, msgs []string) []string {\n\tif o.SignatureKey == \"\" {\n\t\treturn msgs\n\t}\n\n\tcomponents := strings.Split(o.SignatureKey, \":\")\n\tif len(components) != 2 {\n\t\treturn append(msgs, \"invalid signature hash:key spec: \"+\n\t\t\to.SignatureKey)\n\t}\n\n\talgorithm, secretKey := components[0], components[1]\n\tif hash, err := hmacauth.DigestNameToCryptoHash(algorithm); err != nil {\n\t\treturn append(msgs, \"unsupported signature hash algorithm: \"+\n\t\t\to.SignatureKey)\n\t} else {\n\t\to.signatureData = &SignatureData{hash, secretKey}\n\t}\n\treturn msgs\n}\n\nfunc validateCookieName(o *Options, msgs []string) []string {\n\tcookie := &http.Cookie{Name: o.CookieName}\n\tif cookie.String() == \"\" {\n\t\treturn append(msgs, fmt.Sprintf(\"invalid cookie name: %q\", o.CookieName))\n\t}\n\treturn msgs\n}\n\nfunc addPadding(secret string) string {\n\tpadding := len(secret) % 4\n\tswitch padding {\n\tcase 1:\n\t\treturn secret + \"===\"\n\tcase 2:\n\t\treturn secret + \"==\"\n\tcase 3:\n\t\treturn secret + \"=\"\n\tdefault:\n\t\treturn secret\n\t}\n}\n\n// secretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary\nfunc secretBytes(secret string) []byte {\n\tb, err := base64.URLEncoding.DecodeString(addPadding(secret))\n\tif err == nil {\n\t\treturn []byte(addPadding(string(b)))\n\t}\n\treturn []byte(secret)\n}\n"
  },
  {
    "path": "options_test.go",
    "content": "package main\n\nimport (\n\t\"crypto\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc testOptions() *Options {\n\to := NewOptions()\n\to.Upstreams = append(o.Upstreams, \"http://127.0.0.1:8080/\")\n\to.CookieSecret = \"foobar\"\n\to.ClientID = \"bazquux\"\n\to.ClientSecret = \"xyzzyplugh\"\n\to.EmailDomains = []string{\"*\"}\n\treturn o\n}\n\nfunc errorMsg(msgs []string) string {\n\tresult := make([]string, 0)\n\tresult = append(result, \"Invalid configuration:\")\n\tresult = append(result, msgs...)\n\treturn strings.Join(result, \"\\n  \")\n}\n\nfunc TestNewOptions(t *testing.T) {\n\to := NewOptions()\n\to.EmailDomains = []string{\"*\"}\n\terr := o.Validate()\n\tassert.NotEqual(t, nil, err)\n\n\texpected := errorMsg([]string{\n\t\t\"missing setting: cookie-secret\",\n\t\t\"missing setting: client-id\",\n\t\t\"missing setting: client-secret\"})\n\tassert.Equal(t, expected, err.Error())\n}\n\nfunc TestGoogleGroupOptions(t *testing.T) {\n\to := testOptions()\n\to.GoogleGroups = []string{\"googlegroup\"}\n\terr := o.Validate()\n\tassert.NotEqual(t, nil, err)\n\n\texpected := errorMsg([]string{\n\t\t\"missing setting: google-admin-email\",\n\t\t\"missing setting: google-service-account-json\"})\n\tassert.Equal(t, expected, err.Error())\n}\n\nfunc TestGoogleGroupInvalidFile(t *testing.T) {\n\to := testOptions()\n\to.GoogleGroups = []string{\"test_group\"}\n\to.GoogleAdminEmail = \"admin@example.com\"\n\to.GoogleServiceAccountJSON = \"file_doesnt_exist.json\"\n\terr := o.Validate()\n\tassert.NotEqual(t, nil, err)\n\n\texpected := errorMsg([]string{\n\t\t\"invalid Google credentials file: file_doesnt_exist.json\",\n\t})\n\tassert.Equal(t, expected, err.Error())\n}\n\nfunc TestInitializedOptions(t *testing.T) {\n\to := testOptions()\n\tassert.Equal(t, nil, o.Validate())\n}\n\n// Note that it's not worth testing nonparseable URLs, since url.Parse()\n// seems to parse damn near anything.\nfunc TestRedirectURL(t *testing.T) {\n\to := testOptions()\n\to.RedirectURL = \"https://myhost.com/oauth2/callback\"\n\tassert.Equal(t, nil, o.Validate())\n\texpected := &url.URL{\n\t\tScheme: \"https\", Host: \"myhost.com\", Path: \"/oauth2/callback\"}\n\tassert.Equal(t, expected, o.redirectURL)\n}\n\nfunc TestProxyURLs(t *testing.T) {\n\to := testOptions()\n\to.Upstreams = append(o.Upstreams, \"http://127.0.0.1:8081\")\n\tassert.Equal(t, nil, o.Validate())\n\texpected := []*url.URL{\n\t\t&url.URL{Scheme: \"http\", Host: \"127.0.0.1:8080\", Path: \"/\"},\n\t\t// note the '/' was added\n\t\t&url.URL{Scheme: \"http\", Host: \"127.0.0.1:8081\", Path: \"/\"},\n\t}\n\tassert.Equal(t, expected, o.proxyURLs)\n}\n\nfunc TestProxyURLsError(t *testing.T) {\n\to := testOptions()\n\to.Upstreams = append(o.Upstreams, \"127.0.0.1:8081\")\n\terr := o.Validate()\n\tassert.NotEqual(t, nil, err)\n\n\texpected := errorMsg([]string{\n\t\t\"error parsing upstream: parse 127.0.0.1:8081: \" +\n\t\t\t\"first path segment in URL cannot contain colon\"})\n\tassert.Equal(t, expected, err.Error())\n}\n\nfunc TestCompiledRegex(t *testing.T) {\n\to := testOptions()\n\tregexps := []string{\"/foo/.*\", \"/ba[rz]/quux\"}\n\to.SkipAuthRegex = regexps\n\tassert.Equal(t, nil, o.Validate())\n\tactual := make([]string, 0)\n\tfor _, regex := range o.CompiledRegex {\n\t\tactual = append(actual, regex.String())\n\t}\n\tassert.Equal(t, regexps, actual)\n}\n\nfunc TestCompiledRegexError(t *testing.T) {\n\to := testOptions()\n\to.SkipAuthRegex = []string{\"(foobaz\", \"barquux)\"}\n\terr := o.Validate()\n\tassert.NotEqual(t, nil, err)\n\n\texpected := errorMsg([]string{\n\t\t\"error compiling regex=\\\"(foobaz\\\" error parsing regexp: \" +\n\t\t\t\"missing closing ): `(foobaz`\",\n\t\t\"error compiling regex=\\\"barquux)\\\" error parsing regexp: \" +\n\t\t\t\"unexpected ): `barquux)`\"})\n\tassert.Equal(t, expected, err.Error())\n\n\to.SkipAuthRegex = []string{\"foobaz\", \"barquux)\"}\n\terr = o.Validate()\n\tassert.NotEqual(t, nil, err)\n\n\texpected = errorMsg([]string{\n\t\t\"error compiling regex=\\\"barquux)\\\" error parsing regexp: \" +\n\t\t\t\"unexpected ): `barquux)`\"})\n\tassert.Equal(t, expected, err.Error())\n}\n\nfunc TestDefaultProviderApiSettings(t *testing.T) {\n\to := testOptions()\n\tassert.Equal(t, nil, o.Validate())\n\tp := o.provider.Data()\n\tassert.Equal(t, \"https://accounts.google.com/o/oauth2/auth?access_type=offline\",\n\t\tp.LoginURL.String())\n\tassert.Equal(t, \"https://www.googleapis.com/oauth2/v3/token\",\n\t\tp.RedeemURL.String())\n\tassert.Equal(t, \"\", p.ProfileURL.String())\n\tassert.Equal(t, \"profile email\", p.Scope)\n}\n\nfunc TestPassAccessTokenRequiresSpecificCookieSecretLengths(t *testing.T) {\n\to := testOptions()\n\tassert.Equal(t, nil, o.Validate())\n\n\tassert.Equal(t, false, o.PassAccessToken)\n\to.PassAccessToken = true\n\to.CookieSecret = \"cookie of invalid length-\"\n\tassert.NotEqual(t, nil, o.Validate())\n\n\to.PassAccessToken = false\n\to.CookieRefresh = time.Duration(24) * time.Hour\n\tassert.NotEqual(t, nil, o.Validate())\n\n\to.CookieSecret = \"16 bytes AES-128\"\n\tassert.Equal(t, nil, o.Validate())\n\n\to.CookieSecret = \"24 byte secret AES-192--\"\n\tassert.Equal(t, nil, o.Validate())\n\n\to.CookieSecret = \"32 byte secret for AES-256------\"\n\tassert.Equal(t, nil, o.Validate())\n}\n\nfunc TestCookieRefreshMustBeLessThanCookieExpire(t *testing.T) {\n\to := testOptions()\n\tassert.Equal(t, nil, o.Validate())\n\n\to.CookieSecret = \"0123456789abcdefabcd\"\n\to.CookieRefresh = o.CookieExpire\n\tassert.NotEqual(t, nil, o.Validate())\n\n\to.CookieRefresh -= time.Duration(1)\n\tassert.Equal(t, nil, o.Validate())\n}\n\nfunc TestBase64CookieSecret(t *testing.T) {\n\to := testOptions()\n\tassert.Equal(t, nil, o.Validate())\n\n\t// 32 byte, base64 (urlsafe) encoded key\n\to.CookieSecret = \"yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ=\"\n\tassert.Equal(t, nil, o.Validate())\n\n\t// 32 byte, base64 (urlsafe) encoded key, w/o padding\n\to.CookieSecret = \"yHBw2lh2Cvo6aI_jn_qMTr-pRAjtq0nzVgDJNb36jgQ\"\n\tassert.Equal(t, nil, o.Validate())\n\n\t// 24 byte, base64 (urlsafe) encoded key\n\to.CookieSecret = \"Kp33Gj-GQmYtz4zZUyUDdqQKx5_Hgkv3\"\n\tassert.Equal(t, nil, o.Validate())\n\n\t// 16 byte, base64 (urlsafe) encoded key\n\to.CookieSecret = \"LFEqZYvYUwKwzn0tEuTpLA==\"\n\tassert.Equal(t, nil, o.Validate())\n\n\t// 16 byte, base64 (urlsafe) encoded key, w/o padding\n\to.CookieSecret = \"LFEqZYvYUwKwzn0tEuTpLA\"\n\tassert.Equal(t, nil, o.Validate())\n}\n\nfunc TestValidateSignatureKey(t *testing.T) {\n\to := testOptions()\n\to.SignatureKey = \"sha1:secret\"\n\tassert.Equal(t, nil, o.Validate())\n\tassert.Equal(t, o.signatureData.hash, crypto.SHA1)\n\tassert.Equal(t, o.signatureData.key, \"secret\")\n}\n\nfunc TestValidateSignatureKeyInvalidSpec(t *testing.T) {\n\to := testOptions()\n\to.SignatureKey = \"invalid spec\"\n\terr := o.Validate()\n\tassert.Equal(t, err.Error(), \"Invalid configuration:\\n\"+\n\t\t\"  invalid signature hash:key spec: \"+o.SignatureKey)\n}\n\nfunc TestValidateSignatureKeyUnsupportedAlgorithm(t *testing.T) {\n\to := testOptions()\n\to.SignatureKey = \"unsupported:default secret\"\n\terr := o.Validate()\n\tassert.Equal(t, err.Error(), \"Invalid configuration:\\n\"+\n\t\t\"  unsupported signature hash algorithm: \"+o.SignatureKey)\n}\n\nfunc TestValidateCookie(t *testing.T) {\n\to := testOptions()\n\to.CookieName = \"_valid_cookie_name\"\n\tassert.Equal(t, nil, o.Validate())\n}\n\nfunc TestValidateCookieBadName(t *testing.T) {\n\to := testOptions()\n\to.CookieName = \"_bad_cookie_name{}\"\n\terr := o.Validate()\n\tassert.Equal(t, err.Error(), \"Invalid configuration:\\n\"+\n\t\tfmt.Sprintf(\"  invalid cookie name: %q\", o.CookieName))\n}\n"
  },
  {
    "path": "providers/azure.go",
    "content": "package providers\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/bitly/go-simplejson\"\n\t\"github.com/bitly/oauth2_proxy/api\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype AzureProvider struct {\n\t*ProviderData\n\tTenant string\n}\n\nfunc NewAzureProvider(p *ProviderData) *AzureProvider {\n\tp.ProviderName = \"Azure\"\n\n\tif p.ProfileURL == nil || p.ProfileURL.String() == \"\" {\n\t\tp.ProfileURL = &url.URL{\n\t\t\tScheme:   \"https\",\n\t\t\tHost:     \"graph.windows.net\",\n\t\t\tPath:     \"/me\",\n\t\t\tRawQuery: \"api-version=1.6\",\n\t\t}\n\t}\n\tif p.ProtectedResource == nil || p.ProtectedResource.String() == \"\" {\n\t\tp.ProtectedResource = &url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   \"graph.windows.net\",\n\t\t}\n\t}\n\tif p.Scope == \"\" {\n\t\tp.Scope = \"openid\"\n\t}\n\n\treturn &AzureProvider{ProviderData: p}\n}\n\nfunc (p *AzureProvider) Configure(tenant string) {\n\tp.Tenant = tenant\n\tif tenant == \"\" {\n\t\tp.Tenant = \"common\"\n\t}\n\n\tif p.LoginURL == nil || p.LoginURL.String() == \"\" {\n\t\tp.LoginURL = &url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   \"login.microsoftonline.com\",\n\t\t\tPath:   \"/\" + p.Tenant + \"/oauth2/authorize\"}\n\t}\n\tif p.RedeemURL == nil || p.RedeemURL.String() == \"\" {\n\t\tp.RedeemURL = &url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   \"login.microsoftonline.com\",\n\t\t\tPath:   \"/\" + p.Tenant + \"/oauth2/token\",\n\t\t}\n\t}\n}\n\nfunc getAzureHeader(access_token string) http.Header {\n\theader := make(http.Header)\n\theader.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", access_token))\n\treturn header\n}\n\nfunc getEmailFromJSON(json *simplejson.Json) (string, error) {\n\tvar email string\n\tvar err error\n\n\temail, err = json.Get(\"mail\").String()\n\n\tif err != nil || email == \"\" {\n\t\totherMails, otherMailsErr := json.Get(\"otherMails\").Array()\n\t\tif len(otherMails) > 0 {\n\t\t\temail = otherMails[0].(string)\n\t\t}\n\t\terr = otherMailsErr\n\t}\n\n\treturn email, err\n}\n\nfunc (p *AzureProvider) GetEmailAddress(s *SessionState) (string, error) {\n\tvar email string\n\tvar err error\n\n\tif s.AccessToken == \"\" {\n\t\treturn \"\", errors.New(\"missing access token\")\n\t}\n\treq, err := http.NewRequest(\"GET\", p.ProfileURL.String(), nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq.Header = getAzureHeader(s.AccessToken)\n\n\tjson, err := api.Request(req)\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\temail, err = getEmailFromJSON(json)\n\n\tif err == nil && email != \"\" {\n\t\treturn email, err\n\t}\n\n\temail, err = json.Get(\"userPrincipalName\").String()\n\n\tif err != nil {\n\t\tlog.Printf(\"failed making request %s\", err)\n\t\treturn \"\", err\n\t}\n\n\tif email == \"\" {\n\t\tlog.Printf(\"failed to get email address\")\n\t\treturn \"\", err\n\t}\n\n\treturn email, err\n}\n"
  },
  {
    "path": "providers/azure_test.go",
    "content": "package providers\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc testAzureProvider(hostname string) *AzureProvider {\n\tp := NewAzureProvider(\n\t\t&ProviderData{\n\t\t\tProviderName:      \"\",\n\t\t\tLoginURL:          &url.URL{},\n\t\t\tRedeemURL:         &url.URL{},\n\t\t\tProfileURL:        &url.URL{},\n\t\t\tValidateURL:       &url.URL{},\n\t\t\tProtectedResource: &url.URL{},\n\t\t\tScope:             \"\"})\n\tif hostname != \"\" {\n\t\tupdateURL(p.Data().LoginURL, hostname)\n\t\tupdateURL(p.Data().RedeemURL, hostname)\n\t\tupdateURL(p.Data().ProfileURL, hostname)\n\t\tupdateURL(p.Data().ValidateURL, hostname)\n\t\tupdateURL(p.Data().ProtectedResource, hostname)\n\t}\n\treturn p\n}\n\nfunc TestAzureProviderDefaults(t *testing.T) {\n\tp := testAzureProvider(\"\")\n\tassert.NotEqual(t, nil, p)\n\tp.Configure(\"\")\n\tassert.Equal(t, \"Azure\", p.Data().ProviderName)\n\tassert.Equal(t, \"common\", p.Tenant)\n\tassert.Equal(t, \"https://login.microsoftonline.com/common/oauth2/authorize\",\n\t\tp.Data().LoginURL.String())\n\tassert.Equal(t, \"https://login.microsoftonline.com/common/oauth2/token\",\n\t\tp.Data().RedeemURL.String())\n\tassert.Equal(t, \"https://graph.windows.net/me?api-version=1.6\",\n\t\tp.Data().ProfileURL.String())\n\tassert.Equal(t, \"https://graph.windows.net\",\n\t\tp.Data().ProtectedResource.String())\n\tassert.Equal(t, \"\",\n\t\tp.Data().ValidateURL.String())\n\tassert.Equal(t, \"openid\", p.Data().Scope)\n}\n\nfunc TestAzureProviderOverrides(t *testing.T) {\n\tp := NewAzureProvider(\n\t\t&ProviderData{\n\t\t\tLoginURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/oauth/auth\"},\n\t\t\tRedeemURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/oauth/token\"},\n\t\t\tProfileURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/oauth/profile\"},\n\t\t\tValidateURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/oauth/tokeninfo\"},\n\t\t\tProtectedResource: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\"},\n\t\t\tScope: \"profile\"})\n\tassert.NotEqual(t, nil, p)\n\tassert.Equal(t, \"Azure\", p.Data().ProviderName)\n\tassert.Equal(t, \"https://example.com/oauth/auth\",\n\t\tp.Data().LoginURL.String())\n\tassert.Equal(t, \"https://example.com/oauth/token\",\n\t\tp.Data().RedeemURL.String())\n\tassert.Equal(t, \"https://example.com/oauth/profile\",\n\t\tp.Data().ProfileURL.String())\n\tassert.Equal(t, \"https://example.com/oauth/tokeninfo\",\n\t\tp.Data().ValidateURL.String())\n\tassert.Equal(t, \"https://example.com\",\n\t\tp.Data().ProtectedResource.String())\n\tassert.Equal(t, \"profile\", p.Data().Scope)\n}\n\nfunc TestAzureSetTenant(t *testing.T) {\n\tp := testAzureProvider(\"\")\n\tp.Configure(\"example\")\n\tassert.Equal(t, \"Azure\", p.Data().ProviderName)\n\tassert.Equal(t, \"example\", p.Tenant)\n\tassert.Equal(t, \"https://login.microsoftonline.com/example/oauth2/authorize\",\n\t\tp.Data().LoginURL.String())\n\tassert.Equal(t, \"https://login.microsoftonline.com/example/oauth2/token\",\n\t\tp.Data().RedeemURL.String())\n\tassert.Equal(t, \"https://graph.windows.net/me?api-version=1.6\",\n\t\tp.Data().ProfileURL.String())\n\tassert.Equal(t, \"https://graph.windows.net\",\n\t\tp.Data().ProtectedResource.String())\n\tassert.Equal(t, \"\",\n\t\tp.Data().ValidateURL.String())\n\tassert.Equal(t, \"openid\", p.Data().Scope)\n}\n\nfunc testAzureBackend(payload string) *httptest.Server {\n\tpath := \"/me\"\n\tquery := \"api-version=1.6\"\n\n\treturn httptest.NewServer(http.HandlerFunc(\n\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\turl := r.URL\n\t\t\tif url.Path != path || url.RawQuery != query {\n\t\t\t\tw.WriteHeader(404)\n\t\t\t} else if r.Header.Get(\"Authorization\") != \"Bearer imaginary_access_token\" {\n\t\t\t\tw.WriteHeader(403)\n\t\t\t} else {\n\t\t\t\tw.WriteHeader(200)\n\t\t\t\tw.Write([]byte(payload))\n\t\t\t}\n\t\t}))\n}\n\nfunc TestAzureProviderGetEmailAddress(t *testing.T) {\n\tb := testAzureBackend(`{ \"mail\": \"user@windows.net\" }`)\n\tdefer b.Close()\n\n\tbURL, _ := url.Parse(b.URL)\n\tp := testAzureProvider(bURL.Host)\n\n\tsession := &SessionState{AccessToken: \"imaginary_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, \"user@windows.net\", email)\n}\n\nfunc TestAzureProviderGetEmailAddressMailNull(t *testing.T) {\n\tb := testAzureBackend(`{ \"mail\": null, \"otherMails\": [\"user@windows.net\", \"altuser@windows.net\"] }`)\n\tdefer b.Close()\n\n\tbURL, _ := url.Parse(b.URL)\n\tp := testAzureProvider(bURL.Host)\n\n\tsession := &SessionState{AccessToken: \"imaginary_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, \"user@windows.net\", email)\n}\n\nfunc TestAzureProviderGetEmailAddressGetUserPrincipalName(t *testing.T) {\n\tb := testAzureBackend(`{ \"mail\": null, \"otherMails\": [], \"userPrincipalName\": \"user@windows.net\" }`)\n\tdefer b.Close()\n\n\tbURL, _ := url.Parse(b.URL)\n\tp := testAzureProvider(bURL.Host)\n\n\tsession := &SessionState{AccessToken: \"imaginary_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, \"user@windows.net\", email)\n}\n\nfunc TestAzureProviderGetEmailAddressFailToGetEmailAddress(t *testing.T) {\n\tb := testAzureBackend(`{ \"mail\": null, \"otherMails\": [], \"userPrincipalName\": null }`)\n\tdefer b.Close()\n\n\tbURL, _ := url.Parse(b.URL)\n\tp := testAzureProvider(bURL.Host)\n\n\tsession := &SessionState{AccessToken: \"imaginary_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.Equal(t, \"type assertion to string failed\", err.Error())\n\tassert.Equal(t, \"\", email)\n}\n\nfunc TestAzureProviderGetEmailAddressEmptyUserPrincipalName(t *testing.T) {\n\tb := testAzureBackend(`{ \"mail\": null, \"otherMails\": [], \"userPrincipalName\": \"\" }`)\n\tdefer b.Close()\n\n\tbURL, _ := url.Parse(b.URL)\n\tp := testAzureProvider(bURL.Host)\n\n\tsession := &SessionState{AccessToken: \"imaginary_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, \"\", email)\n}\n\nfunc TestAzureProviderGetEmailAddressIncorrectOtherMails(t *testing.T) {\n\tb := testAzureBackend(`{ \"mail\": null, \"otherMails\": \"\", \"userPrincipalName\": null }`)\n\tdefer b.Close()\n\n\tbURL, _ := url.Parse(b.URL)\n\tp := testAzureProvider(bURL.Host)\n\n\tsession := &SessionState{AccessToken: \"imaginary_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.Equal(t, \"type assertion to string failed\", err.Error())\n\tassert.Equal(t, \"\", email)\n}\n"
  },
  {
    "path": "providers/facebook.go",
    "content": "package providers\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/bitly/oauth2_proxy/api\"\n)\n\ntype FacebookProvider struct {\n\t*ProviderData\n}\n\nfunc NewFacebookProvider(p *ProviderData) *FacebookProvider {\n\tp.ProviderName = \"Facebook\"\n\tif p.LoginURL.String() == \"\" {\n\t\tp.LoginURL = &url.URL{Scheme: \"https\",\n\t\t\tHost: \"www.facebook.com\",\n\t\t\tPath: \"/v2.5/dialog/oauth\",\n\t\t\t// ?granted_scopes=true\n\t\t}\n\t}\n\tif p.RedeemURL.String() == \"\" {\n\t\tp.RedeemURL = &url.URL{Scheme: \"https\",\n\t\t\tHost: \"graph.facebook.com\",\n\t\t\tPath: \"/v2.5/oauth/access_token\",\n\t\t}\n\t}\n\tif p.ProfileURL.String() == \"\" {\n\t\tp.ProfileURL = &url.URL{Scheme: \"https\",\n\t\t\tHost: \"graph.facebook.com\",\n\t\t\tPath: \"/v2.5/me\",\n\t\t}\n\t}\n\tif p.ValidateURL.String() == \"\" {\n\t\tp.ValidateURL = p.ProfileURL\n\t}\n\tif p.Scope == \"\" {\n\t\tp.Scope = \"public_profile email\"\n\t}\n\treturn &FacebookProvider{ProviderData: p}\n}\n\nfunc getFacebookHeader(access_token string) http.Header {\n\theader := make(http.Header)\n\theader.Set(\"Accept\", \"application/json\")\n\theader.Set(\"x-li-format\", \"json\")\n\theader.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", access_token))\n\treturn header\n}\n\nfunc (p *FacebookProvider) GetEmailAddress(s *SessionState) (string, error) {\n\tif s.AccessToken == \"\" {\n\t\treturn \"\", errors.New(\"missing access token\")\n\t}\n\treq, err := http.NewRequest(\"GET\", p.ProfileURL.String()+\"?fields=name,email\", nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq.Header = getFacebookHeader(s.AccessToken)\n\n\ttype result struct {\n\t\tEmail string\n\t}\n\tvar r result\n\terr = api.RequestJson(req, &r)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif r.Email == \"\" {\n\t\treturn \"\", errors.New(\"no email\")\n\t}\n\treturn r.Email, nil\n}\n\nfunc (p *FacebookProvider) ValidateSessionState(s *SessionState) bool {\n\treturn validateToken(p, s.AccessToken, getFacebookHeader(s.AccessToken))\n}\n"
  },
  {
    "path": "providers/github.go",
    "content": "package providers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype GitHubProvider struct {\n\t*ProviderData\n\tOrg  string\n\tTeam string\n}\n\nfunc NewGitHubProvider(p *ProviderData) *GitHubProvider {\n\tp.ProviderName = \"GitHub\"\n\tif p.LoginURL == nil || p.LoginURL.String() == \"\" {\n\t\tp.LoginURL = &url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   \"github.com\",\n\t\t\tPath:   \"/login/oauth/authorize\",\n\t\t}\n\t}\n\tif p.RedeemURL == nil || p.RedeemURL.String() == \"\" {\n\t\tp.RedeemURL = &url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   \"github.com\",\n\t\t\tPath:   \"/login/oauth/access_token\",\n\t\t}\n\t}\n\t// ValidationURL is the API Base URL\n\tif p.ValidateURL == nil || p.ValidateURL.String() == \"\" {\n\t\tp.ValidateURL = &url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   \"api.github.com\",\n\t\t\tPath:   \"/\",\n\t\t}\n\t}\n\tif p.Scope == \"\" {\n\t\tp.Scope = \"user:email\"\n\t}\n\treturn &GitHubProvider{ProviderData: p}\n}\nfunc (p *GitHubProvider) SetOrgTeam(org, team string) {\n\tp.Org = org\n\tp.Team = team\n\tif org != \"\" || team != \"\" {\n\t\tp.Scope += \" read:org\"\n\t}\n}\n\nfunc (p *GitHubProvider) hasOrg(accessToken string) (bool, error) {\n\t// https://developer.github.com/v3/orgs/#list-your-organizations\n\n\tvar orgs []struct {\n\t\tLogin string `json:\"login\"`\n\t}\n\n\ttype orgsPage []struct {\n\t\tLogin string `json:\"login\"`\n\t}\n\n\tpn := 1\n\tfor {\n\t\tparams := url.Values{\n\t\t\t\"limit\": {\"200\"},\n\t\t\t\"page\":  {strconv.Itoa(pn)},\n\t\t}\n\n\t\tendpoint := &url.URL{\n\t\t\tScheme:   p.ValidateURL.Scheme,\n\t\t\tHost:     p.ValidateURL.Host,\n\t\t\tPath:     path.Join(p.ValidateURL.Path, \"/user/orgs\"),\n\t\t\tRawQuery: params.Encode(),\n\t\t}\n\t\treq, _ := http.NewRequest(\"GET\", endpoint.String(), nil)\n\t\treq.Header.Set(\"Accept\", \"application/vnd.github.v3+json\")\n\t\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"token %s\", accessToken))\n\t\tresp, err := http.DefaultClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tbody, err := ioutil.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif resp.StatusCode != 200 {\n\t\t\treturn false, fmt.Errorf(\n\t\t\t\t\"got %d from %q %s\", resp.StatusCode, endpoint.String(), body)\n\t\t}\n\n\t\tvar op orgsPage\n\t\tif err := json.Unmarshal(body, &op); err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif len(op) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\torgs = append(orgs, op...)\n\t\tpn += 1\n\t}\n\n\tvar presentOrgs []string\n\tfor _, org := range orgs {\n\t\tif p.Org == org.Login {\n\t\t\tlog.Printf(\"Found Github Organization: %q\", org.Login)\n\t\t\treturn true, nil\n\t\t}\n\t\tpresentOrgs = append(presentOrgs, org.Login)\n\t}\n\n\tlog.Printf(\"Missing Organization:%q in %v\", p.Org, presentOrgs)\n\treturn false, nil\n}\n\nfunc (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) {\n\t// https://developer.github.com/v3/orgs/teams/#list-user-teams\n\n\tvar teams []struct {\n\t\tName string `json:\"name\"`\n\t\tSlug string `json:\"slug\"`\n\t\tOrg  struct {\n\t\t\tLogin string `json:\"login\"`\n\t\t} `json:\"organization\"`\n\t}\n\n\tparams := url.Values{\n\t\t\"limit\": {\"200\"},\n\t}\n\n\tendpoint := &url.URL{\n\t\tScheme:   p.ValidateURL.Scheme,\n\t\tHost:     p.ValidateURL.Host,\n\t\tPath:     path.Join(p.ValidateURL.Path, \"/user/teams\"),\n\t\tRawQuery: params.Encode(),\n\t}\n\treq, _ := http.NewRequest(\"GET\", endpoint.String(), nil)\n\treq.Header.Set(\"Accept\", \"application/vnd.github.v3+json\")\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"token %s\", accessToken))\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tresp.Body.Close()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif resp.StatusCode != 200 {\n\t\treturn false, fmt.Errorf(\n\t\t\t\"got %d from %q %s\", resp.StatusCode, endpoint.String(), body)\n\t}\n\n\tif err := json.Unmarshal(body, &teams); err != nil {\n\t\treturn false, fmt.Errorf(\"%s unmarshaling %s\", err, body)\n\t}\n\n\tvar hasOrg bool\n\tpresentOrgs := make(map[string]bool)\n\tvar presentTeams []string\n\tfor _, team := range teams {\n\t\tpresentOrgs[team.Org.Login] = true\n\t\tif p.Org == team.Org.Login {\n\t\t\thasOrg = true\n\t\t\tts := strings.Split(p.Team, \",\")\n\t\t\tfor _, t := range ts {\n\t\t\t\tif t == team.Slug {\n\t\t\t\t\tlog.Printf(\"Found Github Organization:%q Team:%q (Name:%q)\", team.Org.Login, team.Slug, team.Name)\n\t\t\t\t\treturn true, nil\n\t\t\t\t}\n\t\t\t}\n\t\t\tpresentTeams = append(presentTeams, team.Slug)\n\t\t}\n\t}\n\tif hasOrg {\n\t\tlog.Printf(\"Missing Team:%q from Org:%q in teams: %v\", p.Team, p.Org, presentTeams)\n\t} else {\n\t\tvar allOrgs []string\n\t\tfor org, _ := range presentOrgs {\n\t\t\tallOrgs = append(allOrgs, org)\n\t\t}\n\t\tlog.Printf(\"Missing Organization:%q in %#v\", p.Org, allOrgs)\n\t}\n\treturn false, nil\n}\n\nfunc (p *GitHubProvider) GetEmailAddress(s *SessionState) (string, error) {\n\n\tvar emails []struct {\n\t\tEmail   string `json:\"email\"`\n\t\tPrimary bool   `json:\"primary\"`\n\t}\n\n\t// if we require an Org or Team, check that first\n\tif p.Org != \"\" {\n\t\tif p.Team != \"\" {\n\t\t\tif ok, err := p.hasOrgAndTeam(s.AccessToken); err != nil || !ok {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t} else {\n\t\t\tif ok, err := p.hasOrg(s.AccessToken); err != nil || !ok {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t}\n\t}\n\n\tendpoint := &url.URL{\n\t\tScheme: p.ValidateURL.Scheme,\n\t\tHost:   p.ValidateURL.Host,\n\t\tPath:   path.Join(p.ValidateURL.Path, \"/user/emails\"),\n\t}\n\treq, _ := http.NewRequest(\"GET\", endpoint.String(), nil)\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"token %s\", s.AccessToken))\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tresp.Body.Close()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif resp.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"got %d from %q %s\",\n\t\t\tresp.StatusCode, endpoint.String(), body)\n\t}\n\n\tlog.Printf(\"got %d from %q %s\", resp.StatusCode, endpoint.String(), body)\n\n\tif err := json.Unmarshal(body, &emails); err != nil {\n\t\treturn \"\", fmt.Errorf(\"%s unmarshaling %s\", err, body)\n\t}\n\n\tfor _, email := range emails {\n\t\tif email.Primary {\n\t\t\treturn email.Email, nil\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n\nfunc (p *GitHubProvider) GetUserName(s *SessionState) (string, error) {\n\tvar user struct {\n\t\tLogin string `json:\"login\"`\n\t\tEmail string `json:\"email\"`\n\t}\n\n\tendpoint := &url.URL{\n\t\tScheme: p.ValidateURL.Scheme,\n\t\tHost:   p.ValidateURL.Host,\n\t\tPath:   path.Join(p.ValidateURL.Path, \"/user\"),\n\t}\n\n\treq, err := http.NewRequest(\"GET\", endpoint.String(), nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not create new GET request: %v\", err)\n\t}\n\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"token %s\", s.AccessToken))\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tdefer resp.Body.Close()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif resp.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"got %d from %q %s\",\n\t\t\tresp.StatusCode, endpoint.String(), body)\n\t}\n\n\tlog.Printf(\"got %d from %q %s\", resp.StatusCode, endpoint.String(), body)\n\n\tif err := json.Unmarshal(body, &user); err != nil {\n\t\treturn \"\", fmt.Errorf(\"%s unmarshaling %s\", err, body)\n\t}\n\n\treturn user.Login, nil\n}\n"
  },
  {
    "path": "providers/github_test.go",
    "content": "package providers\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc testGitHubProvider(hostname string) *GitHubProvider {\n\tp := NewGitHubProvider(\n\t\t&ProviderData{\n\t\t\tProviderName: \"\",\n\t\t\tLoginURL:     &url.URL{},\n\t\t\tRedeemURL:    &url.URL{},\n\t\t\tProfileURL:   &url.URL{},\n\t\t\tValidateURL:  &url.URL{},\n\t\t\tScope:        \"\"})\n\tif hostname != \"\" {\n\t\tupdateURL(p.Data().LoginURL, hostname)\n\t\tupdateURL(p.Data().RedeemURL, hostname)\n\t\tupdateURL(p.Data().ProfileURL, hostname)\n\t\tupdateURL(p.Data().ValidateURL, hostname)\n\t}\n\treturn p\n}\n\nfunc testGitHubBackend(payload []string) *httptest.Server {\n\tpathToQueryMap := map[string][]string{\n\t\t\"/user\":        []string{\"\"},\n\t\t\"/user/emails\": []string{\"\"},\n\t\t\"/user/orgs\":   []string{\"limit=200&page=1\", \"limit=200&page=2\", \"limit=200&page=3\"},\n\t}\n\n\treturn httptest.NewServer(http.HandlerFunc(\n\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\turl := r.URL\n\t\t\tquery, ok := pathToQueryMap[url.Path]\n\t\t\tvalidQuery := false\n\t\t\tindex := 0\n\t\t\tfor i, q := range query {\n\t\t\t\tif q == url.RawQuery {\n\t\t\t\t\tvalidQuery = true\n\t\t\t\t\tindex = i\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !ok {\n\t\t\t\tw.WriteHeader(404)\n\t\t\t} else if !validQuery {\n\t\t\t\tw.WriteHeader(404)\n\t\t\t} else {\n\t\t\t\tw.WriteHeader(200)\n\t\t\t\tw.Write([]byte(payload[index]))\n\t\t\t}\n\t\t}))\n}\n\nfunc TestGitHubProviderDefaults(t *testing.T) {\n\tp := testGitHubProvider(\"\")\n\tassert.NotEqual(t, nil, p)\n\tassert.Equal(t, \"GitHub\", p.Data().ProviderName)\n\tassert.Equal(t, \"https://github.com/login/oauth/authorize\",\n\t\tp.Data().LoginURL.String())\n\tassert.Equal(t, \"https://github.com/login/oauth/access_token\",\n\t\tp.Data().RedeemURL.String())\n\tassert.Equal(t, \"https://api.github.com/\",\n\t\tp.Data().ValidateURL.String())\n\tassert.Equal(t, \"user:email\", p.Data().Scope)\n}\n\nfunc TestGitHubProviderOverrides(t *testing.T) {\n\tp := NewGitHubProvider(\n\t\t&ProviderData{\n\t\t\tLoginURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/login/oauth/authorize\"},\n\t\t\tRedeemURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/login/oauth/access_token\"},\n\t\t\tValidateURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"api.example.com\",\n\t\t\t\tPath:   \"/\"},\n\t\t\tScope: \"profile\"})\n\tassert.NotEqual(t, nil, p)\n\tassert.Equal(t, \"GitHub\", p.Data().ProviderName)\n\tassert.Equal(t, \"https://example.com/login/oauth/authorize\",\n\t\tp.Data().LoginURL.String())\n\tassert.Equal(t, \"https://example.com/login/oauth/access_token\",\n\t\tp.Data().RedeemURL.String())\n\tassert.Equal(t, \"https://api.example.com/\",\n\t\tp.Data().ValidateURL.String())\n\tassert.Equal(t, \"profile\", p.Data().Scope)\n}\n\nfunc TestGitHubProviderGetEmailAddress(t *testing.T) {\n\tb := testGitHubBackend([]string{`[ {\"email\": \"michael.bland@gsa.gov\", \"primary\": true} ]`})\n\tdefer b.Close()\n\n\tbURL, _ := url.Parse(b.URL)\n\tp := testGitHubProvider(bURL.Host)\n\n\tsession := &SessionState{AccessToken: \"imaginary_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, \"michael.bland@gsa.gov\", email)\n}\n\nfunc TestGitHubProviderGetEmailAddressWithOrg(t *testing.T) {\n\tb := testGitHubBackend([]string{\n\t\t`[ {\"email\": \"michael.bland@gsa.gov\", \"primary\": true, \"login\":\"testorg\"} ]`,\n\t\t`[ {\"email\": \"michael.bland1@gsa.gov\", \"primary\": true, \"login\":\"testorg1\"} ]`,\n\t\t`[ ]`,\n\t})\n\tdefer b.Close()\n\n\tbURL, _ := url.Parse(b.URL)\n\tp := testGitHubProvider(bURL.Host)\n\tp.Org = \"testorg1\"\n\n\tsession := &SessionState{AccessToken: \"imaginary_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, \"michael.bland@gsa.gov\", email)\n}\n\n// Note that trying to trigger the \"failed building request\" case is not\n// practical, since the only way it can fail is if the URL fails to parse.\nfunc TestGitHubProviderGetEmailAddressFailedRequest(t *testing.T) {\n\tb := testGitHubBackend([]string{\"unused payload\"})\n\tdefer b.Close()\n\n\tbURL, _ := url.Parse(b.URL)\n\tp := testGitHubProvider(bURL.Host)\n\n\t// We'll trigger a request failure by using an unexpected access\n\t// token. Alternatively, we could allow the parsing of the payload as\n\t// JSON to fail.\n\tsession := &SessionState{AccessToken: \"unexpected_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.NotEqual(t, nil, err)\n\tassert.Equal(t, \"\", email)\n}\n\nfunc TestGitHubProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {\n\tb := testGitHubBackend([]string{\"{\\\"foo\\\": \\\"bar\\\"}\"})\n\tdefer b.Close()\n\n\tbURL, _ := url.Parse(b.URL)\n\tp := testGitHubProvider(bURL.Host)\n\n\tsession := &SessionState{AccessToken: \"imaginary_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.NotEqual(t, nil, err)\n\tassert.Equal(t, \"\", email)\n}\n\nfunc TestGitHubProviderGetUserName(t *testing.T) {\n\tb := testGitHubBackend([]string{`{\"email\": \"michael.bland@gsa.gov\", \"login\": \"mbland\"}`})\n\tdefer b.Close()\n\n\tbURL, _ := url.Parse(b.URL)\n\tp := testGitHubProvider(bURL.Host)\n\n\tsession := &SessionState{AccessToken: \"imaginary_access_token\"}\n\temail, err := p.GetUserName(session)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, \"mbland\", email)\n}\n"
  },
  {
    "path": "providers/gitlab.go",
    "content": "package providers\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/bitly/oauth2_proxy/api\"\n)\n\ntype GitLabProvider struct {\n\t*ProviderData\n}\n\nfunc NewGitLabProvider(p *ProviderData) *GitLabProvider {\n\tp.ProviderName = \"GitLab\"\n\tif p.LoginURL == nil || p.LoginURL.String() == \"\" {\n\t\tp.LoginURL = &url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   \"gitlab.com\",\n\t\t\tPath:   \"/oauth/authorize\",\n\t\t}\n\t}\n\tif p.RedeemURL == nil || p.RedeemURL.String() == \"\" {\n\t\tp.RedeemURL = &url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   \"gitlab.com\",\n\t\t\tPath:   \"/oauth/token\",\n\t\t}\n\t}\n\tif p.ValidateURL == nil || p.ValidateURL.String() == \"\" {\n\t\tp.ValidateURL = &url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   \"gitlab.com\",\n\t\t\tPath:   \"/api/v4/user\",\n\t\t}\n\t}\n\tif p.Scope == \"\" {\n\t\tp.Scope = \"read_user\"\n\t}\n\treturn &GitLabProvider{ProviderData: p}\n}\n\nfunc (p *GitLabProvider) GetEmailAddress(s *SessionState) (string, error) {\n\n\treq, err := http.NewRequest(\"GET\",\n\t\tp.ValidateURL.String()+\"?access_token=\"+s.AccessToken, nil)\n\tif err != nil {\n\t\tlog.Printf(\"failed building request %s\", err)\n\t\treturn \"\", err\n\t}\n\tjson, err := api.Request(req)\n\tif err != nil {\n\t\tlog.Printf(\"failed making request %s\", err)\n\t\treturn \"\", err\n\t}\n\treturn json.Get(\"email\").String()\n}\n"
  },
  {
    "path": "providers/gitlab_test.go",
    "content": "package providers\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc testGitLabProvider(hostname string) *GitLabProvider {\n\tp := NewGitLabProvider(\n\t\t&ProviderData{\n\t\t\tProviderName: \"\",\n\t\t\tLoginURL:     &url.URL{},\n\t\t\tRedeemURL:    &url.URL{},\n\t\t\tProfileURL:   &url.URL{},\n\t\t\tValidateURL:  &url.URL{},\n\t\t\tScope:        \"\"})\n\tif hostname != \"\" {\n\t\tupdateURL(p.Data().LoginURL, hostname)\n\t\tupdateURL(p.Data().RedeemURL, hostname)\n\t\tupdateURL(p.Data().ProfileURL, hostname)\n\t\tupdateURL(p.Data().ValidateURL, hostname)\n\t}\n\treturn p\n}\n\nfunc testGitLabBackend(payload string) *httptest.Server {\n\tpath := \"/api/v4/user\"\n\tquery := \"access_token=imaginary_access_token\"\n\n\treturn httptest.NewServer(http.HandlerFunc(\n\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\turl := r.URL\n\t\t\tif url.Path != path || url.RawQuery != query {\n\t\t\t\tw.WriteHeader(404)\n\t\t\t} else {\n\t\t\t\tw.WriteHeader(200)\n\t\t\t\tw.Write([]byte(payload))\n\t\t\t}\n\t\t}))\n}\n\nfunc TestGitLabProviderDefaults(t *testing.T) {\n\tp := testGitLabProvider(\"\")\n\tassert.NotEqual(t, nil, p)\n\tassert.Equal(t, \"GitLab\", p.Data().ProviderName)\n\tassert.Equal(t, \"https://gitlab.com/oauth/authorize\",\n\t\tp.Data().LoginURL.String())\n\tassert.Equal(t, \"https://gitlab.com/oauth/token\",\n\t\tp.Data().RedeemURL.String())\n\tassert.Equal(t, \"https://gitlab.com/api/v4/user\",\n\t\tp.Data().ValidateURL.String())\n\tassert.Equal(t, \"read_user\", p.Data().Scope)\n}\n\nfunc TestGitLabProviderOverrides(t *testing.T) {\n\tp := NewGitLabProvider(\n\t\t&ProviderData{\n\t\t\tLoginURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/oauth/auth\"},\n\t\t\tRedeemURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/oauth/token\"},\n\t\t\tValidateURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/api/v4/user\"},\n\t\t\tScope: \"profile\"})\n\tassert.NotEqual(t, nil, p)\n\tassert.Equal(t, \"GitLab\", p.Data().ProviderName)\n\tassert.Equal(t, \"https://example.com/oauth/auth\",\n\t\tp.Data().LoginURL.String())\n\tassert.Equal(t, \"https://example.com/oauth/token\",\n\t\tp.Data().RedeemURL.String())\n\tassert.Equal(t, \"https://example.com/api/v4/user\",\n\t\tp.Data().ValidateURL.String())\n\tassert.Equal(t, \"profile\", p.Data().Scope)\n}\n\nfunc TestGitLabProviderGetEmailAddress(t *testing.T) {\n\tb := testGitLabBackend(\"{\\\"email\\\": \\\"michael.bland@gsa.gov\\\"}\")\n\tdefer b.Close()\n\n\tb_url, _ := url.Parse(b.URL)\n\tp := testGitLabProvider(b_url.Host)\n\n\tsession := &SessionState{AccessToken: \"imaginary_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, \"michael.bland@gsa.gov\", email)\n}\n\n// Note that trying to trigger the \"failed building request\" case is not\n// practical, since the only way it can fail is if the URL fails to parse.\nfunc TestGitLabProviderGetEmailAddressFailedRequest(t *testing.T) {\n\tb := testGitLabBackend(\"unused payload\")\n\tdefer b.Close()\n\n\tb_url, _ := url.Parse(b.URL)\n\tp := testGitLabProvider(b_url.Host)\n\n\t// We'll trigger a request failure by using an unexpected access\n\t// token. Alternatively, we could allow the parsing of the payload as\n\t// JSON to fail.\n\tsession := &SessionState{AccessToken: \"unexpected_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.NotEqual(t, nil, err)\n\tassert.Equal(t, \"\", email)\n}\n\nfunc TestGitLabProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {\n\tb := testGitLabBackend(\"{\\\"foo\\\": \\\"bar\\\"}\")\n\tdefer b.Close()\n\n\tb_url, _ := url.Parse(b.URL)\n\tp := testGitLabProvider(b_url.Host)\n\n\tsession := &SessionState{AccessToken: \"imaginary_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.NotEqual(t, nil, err)\n\tassert.Equal(t, \"\", email)\n}\n"
  },
  {
    "path": "providers/google.go",
    "content": "package providers\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/google\"\n\t\"google.golang.org/api/admin/directory/v1\"\n\t\"google.golang.org/api/googleapi\"\n)\n\ntype GoogleProvider struct {\n\t*ProviderData\n\tRedeemRefreshURL *url.URL\n\t// GroupValidator is a function that determines if the passed email is in\n\t// the configured Google group.\n\tGroupValidator func(string) bool\n}\n\nfunc NewGoogleProvider(p *ProviderData) *GoogleProvider {\n\tp.ProviderName = \"Google\"\n\tif p.LoginURL.String() == \"\" {\n\t\tp.LoginURL = &url.URL{Scheme: \"https\",\n\t\t\tHost: \"accounts.google.com\",\n\t\t\tPath: \"/o/oauth2/auth\",\n\t\t\t// to get a refresh token. see https://developers.google.com/identity/protocols/OAuth2WebServer#offline\n\t\t\tRawQuery: \"access_type=offline\",\n\t\t}\n\t}\n\tif p.RedeemURL.String() == \"\" {\n\t\tp.RedeemURL = &url.URL{Scheme: \"https\",\n\t\t\tHost: \"www.googleapis.com\",\n\t\t\tPath: \"/oauth2/v3/token\"}\n\t}\n\tif p.ValidateURL.String() == \"\" {\n\t\tp.ValidateURL = &url.URL{Scheme: \"https\",\n\t\t\tHost: \"www.googleapis.com\",\n\t\t\tPath: \"/oauth2/v1/tokeninfo\"}\n\t}\n\tif p.Scope == \"\" {\n\t\tp.Scope = \"profile email\"\n\t}\n\n\treturn &GoogleProvider{\n\t\tProviderData: p,\n\t\t// Set a default GroupValidator to just always return valid (true), it will\n\t\t// be overwritten if we configured a Google group restriction.\n\t\tGroupValidator: func(email string) bool {\n\t\t\treturn true\n\t\t},\n\t}\n}\n\nfunc emailFromIdToken(idToken string) (string, error) {\n\n\t// id_token is a base64 encode ID token payload\n\t// https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo\n\tjwt := strings.Split(idToken, \".\")\n\tjwtData := strings.TrimSuffix(jwt[1], \"=\")\n\tb, err := base64.RawURLEncoding.DecodeString(jwtData)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar email struct {\n\t\tEmail         string `json:\"email\"`\n\t\tEmailVerified bool   `json:\"email_verified\"`\n\t}\n\terr = json.Unmarshal(b, &email)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif email.Email == \"\" {\n\t\treturn \"\", errors.New(\"missing email\")\n\t}\n\tif !email.EmailVerified {\n\t\treturn \"\", fmt.Errorf(\"email %s not listed as verified\", email.Email)\n\t}\n\treturn email.Email, nil\n}\n\nfunc (p *GoogleProvider) Redeem(redirectURL, code string) (s *SessionState, err error) {\n\tif code == \"\" {\n\t\terr = errors.New(\"missing code\")\n\t\treturn\n\t}\n\n\tparams := url.Values{}\n\tparams.Add(\"redirect_uri\", redirectURL)\n\tparams.Add(\"client_id\", p.ClientID)\n\tparams.Add(\"client_secret\", p.ClientSecret)\n\tparams.Add(\"code\", code)\n\tparams.Add(\"grant_type\", \"authorization_code\")\n\tvar req *http.Request\n\treq, err = http.NewRequest(\"POST\", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))\n\tif err != nil {\n\t\treturn\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar body []byte\n\tbody, err = ioutil.ReadAll(resp.Body)\n\tresp.Body.Close()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif resp.StatusCode != 200 {\n\t\terr = fmt.Errorf(\"got %d from %q %s\", resp.StatusCode, p.RedeemURL.String(), body)\n\t\treturn\n\t}\n\n\tvar jsonResponse struct {\n\t\tAccessToken  string `json:\"access_token\"`\n\t\tRefreshToken string `json:\"refresh_token\"`\n\t\tExpiresIn    int64  `json:\"expires_in\"`\n\t\tIdToken      string `json:\"id_token\"`\n\t}\n\terr = json.Unmarshal(body, &jsonResponse)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar email string\n\temail, err = emailFromIdToken(jsonResponse.IdToken)\n\tif err != nil {\n\t\treturn\n\t}\n\ts = &SessionState{\n\t\tAccessToken:  jsonResponse.AccessToken,\n\t\tExpiresOn:    time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second),\n\t\tRefreshToken: jsonResponse.RefreshToken,\n\t\tEmail:        email,\n\t}\n\treturn\n}\n\n// SetGroupRestriction configures the GoogleProvider to restrict access to the\n// specified group(s). AdminEmail has to be an administrative email on the domain that is\n// checked. CredentialsFile is the path to a json file containing a Google service\n// account credentials.\nfunc (p *GoogleProvider) SetGroupRestriction(groups []string, adminEmail string, credentialsReader io.Reader) {\n\tadminService := getAdminService(adminEmail, credentialsReader)\n\tp.GroupValidator = func(email string) bool {\n\t\treturn userInGroup(adminService, groups, email)\n\t}\n}\n\nfunc getAdminService(adminEmail string, credentialsReader io.Reader) *admin.Service {\n\tdata, err := ioutil.ReadAll(credentialsReader)\n\tif err != nil {\n\t\tlog.Fatal(\"can't read Google credentials file:\", err)\n\t}\n\tconf, err := google.JWTConfigFromJSON(data, admin.AdminDirectoryUserReadonlyScope, admin.AdminDirectoryGroupReadonlyScope)\n\tif err != nil {\n\t\tlog.Fatal(\"can't load Google credentials file:\", err)\n\t}\n\tconf.Subject = adminEmail\n\n\tclient := conf.Client(oauth2.NoContext)\n\tadminService, err := admin.New(client)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\treturn adminService\n}\n\nfunc userInGroup(service *admin.Service, groups []string, email string) bool {\n\tuser, err := fetchUser(service, email)\n\tif err != nil {\n\t\tlog.Printf(\"error fetching user: %v\", err)\n\t\treturn false\n\t}\n\tid := user.Id\n\tcustID := user.CustomerId\n\n\tfor _, group := range groups {\n\t\tmembers, err := fetchGroupMembers(service, group)\n\t\tif err != nil {\n\t\t\tif err, ok := err.(*googleapi.Error); ok && err.Code == 404 {\n\t\t\t\tlog.Printf(\"error fetching members for group %s: group does not exist\", group)\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"error fetching group members: %v\", err)\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\n\t\tfor _, member := range members {\n\t\t\tswitch member.Type {\n\t\t\tcase \"CUSTOMER\":\n\t\t\t\tif member.Id == custID {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\tcase \"USER\":\n\t\t\t\tif member.Id == id {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\nfunc fetchUser(service *admin.Service, email string) (*admin.User, error) {\n\tuser, err := service.Users.Get(email).Do()\n\treturn user, err\n}\n\nfunc fetchGroupMembers(service *admin.Service, group string) ([]*admin.Member, error) {\n\tmembers := []*admin.Member{}\n\tpageToken := \"\"\n\tfor {\n\t\treq := service.Members.List(group)\n\t\tif pageToken != \"\" {\n\t\t\treq.PageToken(pageToken)\n\t\t}\n\t\tr, err := req.Do()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, member := range r.Members {\n\t\t\tmembers = append(members, member)\n\t\t}\n\t\tif r.NextPageToken == \"\" {\n\t\t\tbreak\n\t\t}\n\t\tpageToken = r.NextPageToken\n\t}\n\treturn members, nil\n}\n\n// ValidateGroup validates that the provided email exists in the configured Google\n// group(s).\nfunc (p *GoogleProvider) ValidateGroup(email string) bool {\n\treturn p.GroupValidator(email)\n}\n\nfunc (p *GoogleProvider) RefreshSessionIfNeeded(s *SessionState) (bool, error) {\n\tif s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == \"\" {\n\t\treturn false, nil\n\t}\n\n\tnewToken, duration, err := p.redeemRefreshToken(s.RefreshToken)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// re-check that the user is in the proper google group(s)\n\tif !p.ValidateGroup(s.Email) {\n\t\treturn false, fmt.Errorf(\"%s is no longer in the group(s)\", s.Email)\n\t}\n\n\torigExpiration := s.ExpiresOn\n\ts.AccessToken = newToken\n\ts.ExpiresOn = time.Now().Add(duration).Truncate(time.Second)\n\tlog.Printf(\"refreshed access token %s (expired on %s)\", s, origExpiration)\n\treturn true, nil\n}\n\nfunc (p *GoogleProvider) redeemRefreshToken(refreshToken string) (token string, expires time.Duration, err error) {\n\t// https://developers.google.com/identity/protocols/OAuth2WebServer#refresh\n\tparams := url.Values{}\n\tparams.Add(\"client_id\", p.ClientID)\n\tparams.Add(\"client_secret\", p.ClientSecret)\n\tparams.Add(\"refresh_token\", refreshToken)\n\tparams.Add(\"grant_type\", \"refresh_token\")\n\tvar req *http.Request\n\treq, err = http.NewRequest(\"POST\", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))\n\tif err != nil {\n\t\treturn\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar body []byte\n\tbody, err = ioutil.ReadAll(resp.Body)\n\tresp.Body.Close()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif resp.StatusCode != 200 {\n\t\terr = fmt.Errorf(\"got %d from %q %s\", resp.StatusCode, p.RedeemURL.String(), body)\n\t\treturn\n\t}\n\n\tvar data struct {\n\t\tAccessToken string `json:\"access_token\"`\n\t\tExpiresIn   int64  `json:\"expires_in\"`\n\t}\n\terr = json.Unmarshal(body, &data)\n\tif err != nil {\n\t\treturn\n\t}\n\ttoken = data.AccessToken\n\texpires = time.Duration(data.ExpiresIn) * time.Second\n\treturn\n}\n"
  },
  {
    "path": "providers/google_test.go",
    "content": "package providers\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc newRedeemServer(body []byte) (*url.URL, *httptest.Server) {\n\ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {\n\t\trw.Write(body)\n\t}))\n\tu, _ := url.Parse(s.URL)\n\treturn u, s\n}\n\nfunc newGoogleProvider() *GoogleProvider {\n\treturn NewGoogleProvider(\n\t\t&ProviderData{\n\t\t\tProviderName: \"\",\n\t\t\tLoginURL:     &url.URL{},\n\t\t\tRedeemURL:    &url.URL{},\n\t\t\tProfileURL:   &url.URL{},\n\t\t\tValidateURL:  &url.URL{},\n\t\t\tScope:        \"\"})\n}\n\nfunc TestGoogleProviderDefaults(t *testing.T) {\n\tp := newGoogleProvider()\n\tassert.NotEqual(t, nil, p)\n\tassert.Equal(t, \"Google\", p.Data().ProviderName)\n\tassert.Equal(t, \"https://accounts.google.com/o/oauth2/auth?access_type=offline\",\n\t\tp.Data().LoginURL.String())\n\tassert.Equal(t, \"https://www.googleapis.com/oauth2/v3/token\",\n\t\tp.Data().RedeemURL.String())\n\tassert.Equal(t, \"https://www.googleapis.com/oauth2/v1/tokeninfo\",\n\t\tp.Data().ValidateURL.String())\n\tassert.Equal(t, \"\", p.Data().ProfileURL.String())\n\tassert.Equal(t, \"profile email\", p.Data().Scope)\n}\n\nfunc TestGoogleProviderOverrides(t *testing.T) {\n\tp := NewGoogleProvider(\n\t\t&ProviderData{\n\t\t\tLoginURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/oauth/auth\"},\n\t\t\tRedeemURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/oauth/token\"},\n\t\t\tProfileURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/oauth/profile\"},\n\t\t\tValidateURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/oauth/tokeninfo\"},\n\t\t\tScope: \"profile\"})\n\tassert.NotEqual(t, nil, p)\n\tassert.Equal(t, \"Google\", p.Data().ProviderName)\n\tassert.Equal(t, \"https://example.com/oauth/auth\",\n\t\tp.Data().LoginURL.String())\n\tassert.Equal(t, \"https://example.com/oauth/token\",\n\t\tp.Data().RedeemURL.String())\n\tassert.Equal(t, \"https://example.com/oauth/profile\",\n\t\tp.Data().ProfileURL.String())\n\tassert.Equal(t, \"https://example.com/oauth/tokeninfo\",\n\t\tp.Data().ValidateURL.String())\n\tassert.Equal(t, \"profile\", p.Data().Scope)\n}\n\ntype redeemResponse struct {\n\tAccessToken  string `json:\"access_token\"`\n\tRefreshToken string `json:\"refresh_token\"`\n\tExpiresIn    int64  `json:\"expires_in\"`\n\tIdToken      string `json:\"id_token\"`\n}\n\nfunc TestGoogleProviderGetEmailAddress(t *testing.T) {\n\tp := newGoogleProvider()\n\tbody, err := json.Marshal(redeemResponse{\n\t\tAccessToken:  \"a1234\",\n\t\tExpiresIn:    10,\n\t\tRefreshToken: \"refresh12345\",\n\t\tIdToken:      \"ignored prefix.\" + base64.URLEncoding.EncodeToString([]byte(`{\"email\": \"michael.bland@gsa.gov\", \"email_verified\":true}`)),\n\t})\n\tassert.Equal(t, nil, err)\n\tvar server *httptest.Server\n\tp.RedeemURL, server = newRedeemServer(body)\n\tdefer server.Close()\n\n\tsession, err := p.Redeem(\"http://redirect/\", \"code1234\")\n\tassert.Equal(t, nil, err)\n\tassert.NotEqual(t, session, nil)\n\tassert.Equal(t, \"michael.bland@gsa.gov\", session.Email)\n\tassert.Equal(t, \"a1234\", session.AccessToken)\n\tassert.Equal(t, \"refresh12345\", session.RefreshToken)\n}\n\nfunc TestGoogleProviderValidateGroup(t *testing.T) {\n\tp := newGoogleProvider()\n\tp.GroupValidator = func(email string) bool {\n\t\treturn email == \"michael.bland@gsa.gov\"\n\t}\n\tassert.Equal(t, true, p.ValidateGroup(\"michael.bland@gsa.gov\"))\n\tp.GroupValidator = func(email string) bool {\n\t\treturn email != \"michael.bland@gsa.gov\"\n\t}\n\tassert.Equal(t, false, p.ValidateGroup(\"michael.bland@gsa.gov\"))\n}\n\nfunc TestGoogleProviderWithoutValidateGroup(t *testing.T) {\n\tp := newGoogleProvider()\n\tassert.Equal(t, true, p.ValidateGroup(\"michael.bland@gsa.gov\"))\n}\n\n//\nfunc TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) {\n\tp := newGoogleProvider()\n\tbody, err := json.Marshal(redeemResponse{\n\t\tAccessToken: \"a1234\",\n\t\tIdToken:     \"ignored prefix.\" + `{\"email\": \"michael.bland@gsa.gov\"}`,\n\t})\n\tassert.Equal(t, nil, err)\n\tvar server *httptest.Server\n\tp.RedeemURL, server = newRedeemServer(body)\n\tdefer server.Close()\n\n\tsession, err := p.Redeem(\"http://redirect/\", \"code1234\")\n\tassert.NotEqual(t, nil, err)\n\tif session != nil {\n\t\tt.Errorf(\"expect nill session %#v\", session)\n\t}\n}\n\nfunc TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) {\n\tp := newGoogleProvider()\n\n\tbody, err := json.Marshal(redeemResponse{\n\t\tAccessToken: \"a1234\",\n\t\tIdToken:     \"ignored prefix.\" + base64.URLEncoding.EncodeToString([]byte(`{\"email\": michael.bland@gsa.gov}`)),\n\t})\n\tassert.Equal(t, nil, err)\n\tvar server *httptest.Server\n\tp.RedeemURL, server = newRedeemServer(body)\n\tdefer server.Close()\n\n\tsession, err := p.Redeem(\"http://redirect/\", \"code1234\")\n\tassert.NotEqual(t, nil, err)\n\tif session != nil {\n\t\tt.Errorf(\"expect nill session %#v\", session)\n\t}\n\n}\n\nfunc TestGoogleProviderGetEmailAddressEmailMissing(t *testing.T) {\n\tp := newGoogleProvider()\n\tbody, err := json.Marshal(redeemResponse{\n\t\tAccessToken: \"a1234\",\n\t\tIdToken:     \"ignored prefix.\" + base64.URLEncoding.EncodeToString([]byte(`{\"not_email\": \"missing\"}`)),\n\t})\n\tassert.Equal(t, nil, err)\n\tvar server *httptest.Server\n\tp.RedeemURL, server = newRedeemServer(body)\n\tdefer server.Close()\n\n\tsession, err := p.Redeem(\"http://redirect/\", \"code1234\")\n\tassert.NotEqual(t, nil, err)\n\tif session != nil {\n\t\tt.Errorf(\"expect nill session %#v\", session)\n\t}\n\n}\n"
  },
  {
    "path": "providers/internal_util.go",
    "content": "package providers\n\nimport (\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/bitly/oauth2_proxy/api\"\n)\n\n// stripToken is a helper function to obfuscate \"access_token\"\n// query parameters\nfunc stripToken(endpoint string) string {\n\treturn stripParam(\"access_token\", endpoint)\n}\n\n// stripParam generalizes the obfuscation of a particular\n// query parameter - typically 'access_token' or 'client_secret'\n// The parameter's second half is replaced by '...' and returned\n// as part of the encoded query parameters.\n// If the target parameter isn't found, the endpoint is returned\n// unmodified.\nfunc stripParam(param, endpoint string) string {\n\tu, err := url.Parse(endpoint)\n\tif err != nil {\n\t\tlog.Printf(\"error attempting to strip %s: %s\", param, err)\n\t\treturn endpoint\n\t}\n\n\tif u.RawQuery != \"\" {\n\t\tvalues, err := url.ParseQuery(u.RawQuery)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"error attempting to strip %s: %s\", param, err)\n\t\t\treturn u.String()\n\t\t}\n\n\t\tif val := values.Get(param); val != \"\" {\n\t\t\tvalues.Set(param, val[:(len(val)/2)]+\"...\")\n\t\t\tu.RawQuery = values.Encode()\n\t\t\treturn u.String()\n\t\t}\n\t}\n\n\treturn endpoint\n}\n\n// validateToken returns true if token is valid\nfunc validateToken(p Provider, access_token string, header http.Header) bool {\n\tif access_token == \"\" || p.Data().ValidateURL == nil {\n\t\treturn false\n\t}\n\tendpoint := p.Data().ValidateURL.String()\n\tif len(header) == 0 {\n\t\tparams := url.Values{\"access_token\": {access_token}}\n\t\tendpoint = endpoint + \"?\" + params.Encode()\n\t}\n\tresp, err := api.RequestUnparsedResponse(endpoint, header)\n\tif err != nil {\n\t\tlog.Printf(\"GET %s\", stripToken(endpoint))\n\t\tlog.Printf(\"token validation request failed: %s\", err)\n\t\treturn false\n\t}\n\n\tbody, _ := ioutil.ReadAll(resp.Body)\n\tresp.Body.Close()\n\tlog.Printf(\"%d GET %s %s\", resp.StatusCode, stripToken(endpoint), body)\n\n\tif resp.StatusCode == 200 {\n\t\treturn true\n\t}\n\tlog.Printf(\"token validation request failed: status %d - %s\", resp.StatusCode, body)\n\treturn false\n}\n\nfunc updateURL(url *url.URL, hostname string) {\n\turl.Scheme = \"http\"\n\turl.Host = hostname\n}\n"
  },
  {
    "path": "providers/internal_util_test.go",
    "content": "package providers\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype ValidateSessionStateTestProvider struct {\n\t*ProviderData\n}\n\nfunc (tp *ValidateSessionStateTestProvider) GetEmailAddress(s *SessionState) (string, error) {\n\treturn \"\", errors.New(\"not implemented\")\n}\n\n// Note that we're testing the internal validateToken() used to implement\n// several Provider's ValidateSessionState() implementations\nfunc (tp *ValidateSessionStateTestProvider) ValidateSessionState(s *SessionState) bool {\n\treturn false\n}\n\ntype ValidateSessionStateTest struct {\n\tbackend       *httptest.Server\n\tresponse_code int\n\tprovider      *ValidateSessionStateTestProvider\n\theader        http.Header\n}\n\nfunc NewValidateSessionStateTest() *ValidateSessionStateTest {\n\tvar vt_test ValidateSessionStateTest\n\n\tvt_test.backend = httptest.NewServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.URL.Path != \"/oauth/tokeninfo\" {\n\t\t\t\tw.WriteHeader(500)\n\t\t\t\tw.Write([]byte(\"unknown URL\"))\n\t\t\t}\n\t\t\ttoken_param := r.FormValue(\"access_token\")\n\t\t\tif token_param == \"\" {\n\t\t\t\tmissing := false\n\t\t\t\treceived_headers := r.Header\n\t\t\t\tfor k, _ := range vt_test.header {\n\t\t\t\t\treceived := received_headers.Get(k)\n\t\t\t\t\texpected := vt_test.header.Get(k)\n\t\t\t\t\tif received == \"\" || received != expected {\n\t\t\t\t\t\tmissing = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif missing {\n\t\t\t\t\tw.WriteHeader(500)\n\t\t\t\t\tw.Write([]byte(\"no token param and missing or incorrect headers\"))\n\t\t\t\t}\n\t\t\t}\n\t\t\tw.WriteHeader(vt_test.response_code)\n\t\t\tw.Write([]byte(\"only code matters; contents disregarded\"))\n\n\t\t}))\n\tbackend_url, _ := url.Parse(vt_test.backend.URL)\n\tvt_test.provider = &ValidateSessionStateTestProvider{\n\t\tProviderData: &ProviderData{\n\t\t\tValidateURL: &url.URL{\n\t\t\t\tScheme: \"http\",\n\t\t\t\tHost:   backend_url.Host,\n\t\t\t\tPath:   \"/oauth/tokeninfo\",\n\t\t\t},\n\t\t},\n\t}\n\tvt_test.response_code = 200\n\treturn &vt_test\n}\n\nfunc (vt_test *ValidateSessionStateTest) Close() {\n\tvt_test.backend.Close()\n}\n\nfunc TestValidateSessionStateValidToken(t *testing.T) {\n\tvt_test := NewValidateSessionStateTest()\n\tdefer vt_test.Close()\n\tassert.Equal(t, true, validateToken(vt_test.provider, \"foobar\", nil))\n}\n\nfunc TestValidateSessionStateValidTokenWithHeaders(t *testing.T) {\n\tvt_test := NewValidateSessionStateTest()\n\tdefer vt_test.Close()\n\tvt_test.header = make(http.Header)\n\tvt_test.header.Set(\"Authorization\", \"Bearer foobar\")\n\tassert.Equal(t, true,\n\t\tvalidateToken(vt_test.provider, \"foobar\", vt_test.header))\n}\n\nfunc TestValidateSessionStateEmptyToken(t *testing.T) {\n\tvt_test := NewValidateSessionStateTest()\n\tdefer vt_test.Close()\n\tassert.Equal(t, false, validateToken(vt_test.provider, \"\", nil))\n}\n\nfunc TestValidateSessionStateEmptyValidateURL(t *testing.T) {\n\tvt_test := NewValidateSessionStateTest()\n\tdefer vt_test.Close()\n\tvt_test.provider.Data().ValidateURL = nil\n\tassert.Equal(t, false, validateToken(vt_test.provider, \"foobar\", nil))\n}\n\nfunc TestValidateSessionStateRequestNetworkFailure(t *testing.T) {\n\tvt_test := NewValidateSessionStateTest()\n\t// Close immediately to simulate a network failure\n\tvt_test.Close()\n\tassert.Equal(t, false, validateToken(vt_test.provider, \"foobar\", nil))\n}\n\nfunc TestValidateSessionStateExpiredToken(t *testing.T) {\n\tvt_test := NewValidateSessionStateTest()\n\tdefer vt_test.Close()\n\tvt_test.response_code = 401\n\tassert.Equal(t, false, validateToken(vt_test.provider, \"foobar\", nil))\n}\n\nfunc TestStripTokenNotPresent(t *testing.T) {\n\ttest := \"http://local.test/api/test?a=1&b=2\"\n\tassert.Equal(t, test, stripToken(test))\n}\n\nfunc TestStripToken(t *testing.T) {\n\ttest := \"http://local.test/api/test?access_token=deadbeef&b=1&c=2\"\n\texpected := \"http://local.test/api/test?access_token=dead...&b=1&c=2\"\n\tassert.Equal(t, expected, stripToken(test))\n}\n"
  },
  {
    "path": "providers/linkedin.go",
    "content": "package providers\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/bitly/oauth2_proxy/api\"\n)\n\ntype LinkedInProvider struct {\n\t*ProviderData\n}\n\nfunc NewLinkedInProvider(p *ProviderData) *LinkedInProvider {\n\tp.ProviderName = \"LinkedIn\"\n\tif p.LoginURL.String() == \"\" {\n\t\tp.LoginURL = &url.URL{Scheme: \"https\",\n\t\t\tHost: \"www.linkedin.com\",\n\t\t\tPath: \"/uas/oauth2/authorization\"}\n\t}\n\tif p.RedeemURL.String() == \"\" {\n\t\tp.RedeemURL = &url.URL{Scheme: \"https\",\n\t\t\tHost: \"www.linkedin.com\",\n\t\t\tPath: \"/uas/oauth2/accessToken\"}\n\t}\n\tif p.ProfileURL.String() == \"\" {\n\t\tp.ProfileURL = &url.URL{Scheme: \"https\",\n\t\t\tHost: \"www.linkedin.com\",\n\t\t\tPath: \"/v1/people/~/email-address\"}\n\t}\n\tif p.ValidateURL.String() == \"\" {\n\t\tp.ValidateURL = p.ProfileURL\n\t}\n\tif p.Scope == \"\" {\n\t\tp.Scope = \"r_emailaddress r_basicprofile\"\n\t}\n\treturn &LinkedInProvider{ProviderData: p}\n}\n\nfunc getLinkedInHeader(access_token string) http.Header {\n\theader := make(http.Header)\n\theader.Set(\"Accept\", \"application/json\")\n\theader.Set(\"x-li-format\", \"json\")\n\theader.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", access_token))\n\treturn header\n}\n\nfunc (p *LinkedInProvider) GetEmailAddress(s *SessionState) (string, error) {\n\tif s.AccessToken == \"\" {\n\t\treturn \"\", errors.New(\"missing access token\")\n\t}\n\treq, err := http.NewRequest(\"GET\", p.ProfileURL.String()+\"?format=json\", nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq.Header = getLinkedInHeader(s.AccessToken)\n\n\tjson, err := api.Request(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\temail, err := json.String()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn email, nil\n}\n\nfunc (p *LinkedInProvider) ValidateSessionState(s *SessionState) bool {\n\treturn validateToken(p, s.AccessToken, getLinkedInHeader(s.AccessToken))\n}\n"
  },
  {
    "path": "providers/linkedin_test.go",
    "content": "package providers\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc testLinkedInProvider(hostname string) *LinkedInProvider {\n\tp := NewLinkedInProvider(\n\t\t&ProviderData{\n\t\t\tProviderName: \"\",\n\t\t\tLoginURL:     &url.URL{},\n\t\t\tRedeemURL:    &url.URL{},\n\t\t\tProfileURL:   &url.URL{},\n\t\t\tValidateURL:  &url.URL{},\n\t\t\tScope:        \"\"})\n\tif hostname != \"\" {\n\t\tupdateURL(p.Data().LoginURL, hostname)\n\t\tupdateURL(p.Data().RedeemURL, hostname)\n\t\tupdateURL(p.Data().ProfileURL, hostname)\n\t}\n\treturn p\n}\n\nfunc testLinkedInBackend(payload string) *httptest.Server {\n\tpath := \"/v1/people/~/email-address\"\n\n\treturn httptest.NewServer(http.HandlerFunc(\n\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\turl := r.URL\n\t\t\tif url.Path != path {\n\t\t\t\tw.WriteHeader(404)\n\t\t\t} else if r.Header.Get(\"Authorization\") != \"Bearer imaginary_access_token\" {\n\t\t\t\tw.WriteHeader(403)\n\t\t\t} else {\n\t\t\t\tw.WriteHeader(200)\n\t\t\t\tw.Write([]byte(payload))\n\t\t\t}\n\t\t}))\n}\n\nfunc TestLinkedInProviderDefaults(t *testing.T) {\n\tp := testLinkedInProvider(\"\")\n\tassert.NotEqual(t, nil, p)\n\tassert.Equal(t, \"LinkedIn\", p.Data().ProviderName)\n\tassert.Equal(t, \"https://www.linkedin.com/uas/oauth2/authorization\",\n\t\tp.Data().LoginURL.String())\n\tassert.Equal(t, \"https://www.linkedin.com/uas/oauth2/accessToken\",\n\t\tp.Data().RedeemURL.String())\n\tassert.Equal(t, \"https://www.linkedin.com/v1/people/~/email-address\",\n\t\tp.Data().ProfileURL.String())\n\tassert.Equal(t, \"https://www.linkedin.com/v1/people/~/email-address\",\n\t\tp.Data().ValidateURL.String())\n\tassert.Equal(t, \"r_emailaddress r_basicprofile\", p.Data().Scope)\n}\n\nfunc TestLinkedInProviderOverrides(t *testing.T) {\n\tp := NewLinkedInProvider(\n\t\t&ProviderData{\n\t\t\tLoginURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/oauth/auth\"},\n\t\t\tRedeemURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/oauth/token\"},\n\t\t\tProfileURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/oauth/profile\"},\n\t\t\tValidateURL: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"example.com\",\n\t\t\t\tPath:   \"/oauth/tokeninfo\"},\n\t\t\tScope: \"profile\"})\n\tassert.NotEqual(t, nil, p)\n\tassert.Equal(t, \"LinkedIn\", p.Data().ProviderName)\n\tassert.Equal(t, \"https://example.com/oauth/auth\",\n\t\tp.Data().LoginURL.String())\n\tassert.Equal(t, \"https://example.com/oauth/token\",\n\t\tp.Data().RedeemURL.String())\n\tassert.Equal(t, \"https://example.com/oauth/profile\",\n\t\tp.Data().ProfileURL.String())\n\tassert.Equal(t, \"https://example.com/oauth/tokeninfo\",\n\t\tp.Data().ValidateURL.String())\n\tassert.Equal(t, \"profile\", p.Data().Scope)\n}\n\nfunc TestLinkedInProviderGetEmailAddress(t *testing.T) {\n\tb := testLinkedInBackend(`\"user@linkedin.com\"`)\n\tdefer b.Close()\n\n\tb_url, _ := url.Parse(b.URL)\n\tp := testLinkedInProvider(b_url.Host)\n\n\tsession := &SessionState{AccessToken: \"imaginary_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, \"user@linkedin.com\", email)\n}\n\nfunc TestLinkedInProviderGetEmailAddressFailedRequest(t *testing.T) {\n\tb := testLinkedInBackend(\"unused payload\")\n\tdefer b.Close()\n\n\tb_url, _ := url.Parse(b.URL)\n\tp := testLinkedInProvider(b_url.Host)\n\n\t// We'll trigger a request failure by using an unexpected access\n\t// token. Alternatively, we could allow the parsing of the payload as\n\t// JSON to fail.\n\tsession := &SessionState{AccessToken: \"unexpected_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.NotEqual(t, nil, err)\n\tassert.Equal(t, \"\", email)\n}\n\nfunc TestLinkedInProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {\n\tb := testLinkedInBackend(\"{\\\"foo\\\": \\\"bar\\\"}\")\n\tdefer b.Close()\n\n\tb_url, _ := url.Parse(b.URL)\n\tp := testLinkedInProvider(b_url.Host)\n\n\tsession := &SessionState{AccessToken: \"imaginary_access_token\"}\n\temail, err := p.GetEmailAddress(session)\n\tassert.NotEqual(t, nil, err)\n\tassert.Equal(t, \"\", email)\n}\n"
  },
  {
    "path": "providers/oidc.go",
    "content": "package providers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"golang.org/x/oauth2\"\n\n\toidc \"github.com/coreos/go-oidc\"\n)\n\ntype OIDCProvider struct {\n\t*ProviderData\n\n\tVerifier *oidc.IDTokenVerifier\n}\n\nfunc NewOIDCProvider(p *ProviderData) *OIDCProvider {\n\tp.ProviderName = \"OpenID Connect\"\n\treturn &OIDCProvider{ProviderData: p}\n}\n\nfunc (p *OIDCProvider) Redeem(redirectURL, code string) (s *SessionState, err error) {\n\tctx := context.Background()\n\tc := oauth2.Config{\n\t\tClientID:     p.ClientID,\n\t\tClientSecret: p.ClientSecret,\n\t\tEndpoint: oauth2.Endpoint{\n\t\t\tTokenURL: p.RedeemURL.String(),\n\t\t},\n\t\tRedirectURL: redirectURL,\n\t}\n\ttoken, err := c.Exchange(ctx, code)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"token exchange: %v\", err)\n\t}\n\n\trawIDToken, ok := token.Extra(\"id_token\").(string)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"token response did not contain an id_token\")\n\t}\n\n\t// Parse and verify ID Token payload.\n\tidToken, err := p.Verifier.Verify(ctx, rawIDToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not verify id_token: %v\", err)\n\t}\n\n\t// Extract custom claims.\n\tvar claims struct {\n\t\tEmail    string `json:\"email\"`\n\t\tVerified *bool  `json:\"email_verified\"`\n\t}\n\tif err := idToken.Claims(&claims); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse id_token claims: %v\", err)\n\t}\n\n\tif claims.Email == \"\" {\n\t\treturn nil, fmt.Errorf(\"id_token did not contain an email\")\n\t}\n\tif claims.Verified != nil && !*claims.Verified {\n\t\treturn nil, fmt.Errorf(\"email in id_token (%s) isn't verified\", claims.Email)\n\t}\n\n\ts = &SessionState{\n\t\tAccessToken:  token.AccessToken,\n\t\tRefreshToken: token.RefreshToken,\n\t\tExpiresOn:    token.Expiry,\n\t\tEmail:        claims.Email,\n\t}\n\n\treturn\n}\n\nfunc (p *OIDCProvider) RefreshSessionIfNeeded(s *SessionState) (bool, error) {\n\tif s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == \"\" {\n\t\treturn false, nil\n\t}\n\n\torigExpiration := s.ExpiresOn\n\ts.ExpiresOn = time.Now().Add(time.Second).Truncate(time.Second)\n\tfmt.Printf(\"refreshed access token %s (expired on %s)\\n\", s, origExpiration)\n\treturn false, nil\n}\n"
  },
  {
    "path": "providers/provider_data.go",
    "content": "package providers\n\nimport (\n\t\"net/url\"\n)\n\ntype ProviderData struct {\n\tProviderName      string\n\tClientID          string\n\tClientSecret      string\n\tLoginURL          *url.URL\n\tRedeemURL         *url.URL\n\tProfileURL        *url.URL\n\tProtectedResource *url.URL\n\tValidateURL       *url.URL\n\tScope             string\n\tApprovalPrompt    string\n}\n\nfunc (p *ProviderData) Data() *ProviderData { return p }\n"
  },
  {
    "path": "providers/provider_default.go",
    "content": "package providers\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/bitly/oauth2_proxy/cookie\"\n)\n\nfunc (p *ProviderData) Redeem(redirectURL, code string) (s *SessionState, err error) {\n\tif code == \"\" {\n\t\terr = errors.New(\"missing code\")\n\t\treturn\n\t}\n\n\tparams := url.Values{}\n\tparams.Add(\"redirect_uri\", redirectURL)\n\tparams.Add(\"client_id\", p.ClientID)\n\tparams.Add(\"client_secret\", p.ClientSecret)\n\tparams.Add(\"code\", code)\n\tparams.Add(\"grant_type\", \"authorization_code\")\n\tif p.ProtectedResource != nil && p.ProtectedResource.String() != \"\" {\n\t\tparams.Add(\"resource\", p.ProtectedResource.String())\n\t}\n\n\tvar req *http.Request\n\treq, err = http.NewRequest(\"POST\", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))\n\tif err != nil {\n\t\treturn\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tvar resp *http.Response\n\tresp, err = http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar body []byte\n\tbody, err = ioutil.ReadAll(resp.Body)\n\tresp.Body.Close()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif resp.StatusCode != 200 {\n\t\terr = fmt.Errorf(\"got %d from %q %s\", resp.StatusCode, p.RedeemURL.String(), body)\n\t\treturn\n\t}\n\n\t// blindly try json and x-www-form-urlencoded\n\tvar jsonResponse struct {\n\t\tAccessToken string `json:\"access_token\"`\n\t}\n\terr = json.Unmarshal(body, &jsonResponse)\n\tif err == nil {\n\t\ts = &SessionState{\n\t\t\tAccessToken: jsonResponse.AccessToken,\n\t\t}\n\t\treturn\n\t}\n\n\tvar v url.Values\n\tv, err = url.ParseQuery(string(body))\n\tif err != nil {\n\t\treturn\n\t}\n\tif a := v.Get(\"access_token\"); a != \"\" {\n\t\ts = &SessionState{AccessToken: a}\n\t} else {\n\t\terr = fmt.Errorf(\"no access token found %s\", body)\n\t}\n\treturn\n}\n\n// GetLoginURL with typical oauth parameters\nfunc (p *ProviderData) GetLoginURL(redirectURI, state string) string {\n\tvar a url.URL\n\ta = *p.LoginURL\n\tparams, _ := url.ParseQuery(a.RawQuery)\n\tparams.Set(\"redirect_uri\", redirectURI)\n\tparams.Set(\"approval_prompt\", p.ApprovalPrompt)\n\tparams.Add(\"scope\", p.Scope)\n\tparams.Set(\"client_id\", p.ClientID)\n\tparams.Set(\"response_type\", \"code\")\n\tparams.Add(\"state\", state)\n\ta.RawQuery = params.Encode()\n\treturn a.String()\n}\n\n// CookieForSession serializes a session state for storage in a cookie\nfunc (p *ProviderData) CookieForSession(s *SessionState, c *cookie.Cipher) (string, error) {\n\treturn s.EncodeSessionState(c)\n}\n\n// SessionFromCookie deserializes a session from a cookie value\nfunc (p *ProviderData) SessionFromCookie(v string, c *cookie.Cipher) (s *SessionState, err error) {\n\treturn DecodeSessionState(v, c)\n}\n\nfunc (p *ProviderData) GetEmailAddress(s *SessionState) (string, error) {\n\treturn \"\", errors.New(\"not implemented\")\n}\n\n// GetUserName returns the Account username\nfunc (p *ProviderData) GetUserName(s *SessionState) (string, error) {\n\treturn \"\", errors.New(\"not implemented\")\n}\n\n// ValidateGroup validates that the provided email exists in the configured provider\n// email group(s).\nfunc (p *ProviderData) ValidateGroup(email string) bool {\n\treturn true\n}\n\nfunc (p *ProviderData) ValidateSessionState(s *SessionState) bool {\n\treturn validateToken(p, s.AccessToken, nil)\n}\n\n// RefreshSessionIfNeeded\nfunc (p *ProviderData) RefreshSessionIfNeeded(s *SessionState) (bool, error) {\n\treturn false, nil\n}\n"
  },
  {
    "path": "providers/provider_default_test.go",
    "content": "package providers\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRefresh(t *testing.T) {\n\tp := &ProviderData{}\n\trefreshed, err := p.RefreshSessionIfNeeded(&SessionState{\n\t\tExpiresOn: time.Now().Add(time.Duration(-11) * time.Minute),\n\t})\n\tassert.Equal(t, false, refreshed)\n\tassert.Equal(t, nil, err)\n}\n"
  },
  {
    "path": "providers/providers.go",
    "content": "package providers\n\nimport (\n\t\"github.com/bitly/oauth2_proxy/cookie\"\n)\n\ntype Provider interface {\n\tData() *ProviderData\n\tGetEmailAddress(*SessionState) (string, error)\n\tGetUserName(*SessionState) (string, error)\n\tRedeem(string, string) (*SessionState, error)\n\tValidateGroup(string) bool\n\tValidateSessionState(*SessionState) bool\n\tGetLoginURL(redirectURI, finalRedirect string) string\n\tRefreshSessionIfNeeded(*SessionState) (bool, error)\n\tSessionFromCookie(string, *cookie.Cipher) (*SessionState, error)\n\tCookieForSession(*SessionState, *cookie.Cipher) (string, error)\n}\n\nfunc New(provider string, p *ProviderData) Provider {\n\tswitch provider {\n\tcase \"linkedin\":\n\t\treturn NewLinkedInProvider(p)\n\tcase \"facebook\":\n\t\treturn NewFacebookProvider(p)\n\tcase \"github\":\n\t\treturn NewGitHubProvider(p)\n\tcase \"azure\":\n\t\treturn NewAzureProvider(p)\n\tcase \"gitlab\":\n\t\treturn NewGitLabProvider(p)\n\tcase \"oidc\":\n\t\treturn NewOIDCProvider(p)\n\tdefault:\n\t\treturn NewGoogleProvider(p)\n\t}\n}\n"
  },
  {
    "path": "providers/session_state.go",
    "content": "package providers\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bitly/oauth2_proxy/cookie\"\n)\n\ntype SessionState struct {\n\tAccessToken  string\n\tExpiresOn    time.Time\n\tRefreshToken string\n\tEmail        string\n\tUser         string\n}\n\nfunc (s *SessionState) IsExpired() bool {\n\tif !s.ExpiresOn.IsZero() && s.ExpiresOn.Before(time.Now()) {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (s *SessionState) String() string {\n\to := fmt.Sprintf(\"Session{%s\", s.accountInfo())\n\tif s.AccessToken != \"\" {\n\t\to += \" token:true\"\n\t}\n\tif !s.ExpiresOn.IsZero() {\n\t\to += fmt.Sprintf(\" expires:%s\", s.ExpiresOn)\n\t}\n\tif s.RefreshToken != \"\" {\n\t\to += \" refresh_token:true\"\n\t}\n\treturn o + \"}\"\n}\n\nfunc (s *SessionState) EncodeSessionState(c *cookie.Cipher) (string, error) {\n\tif c == nil || s.AccessToken == \"\" {\n\t\treturn s.accountInfo(), nil\n\t}\n\treturn s.EncryptedString(c)\n}\n\nfunc (s *SessionState) accountInfo() string {\n\treturn fmt.Sprintf(\"email:%s user:%s\", s.Email, s.User)\n}\n\nfunc (s *SessionState) EncryptedString(c *cookie.Cipher) (string, error) {\n\tvar err error\n\tif c == nil {\n\t\tpanic(\"error. missing cipher\")\n\t}\n\ta := s.AccessToken\n\tif a != \"\" {\n\t\tif a, err = c.Encrypt(a); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tr := s.RefreshToken\n\tif r != \"\" {\n\t\tif r, err = c.Encrypt(r); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\treturn fmt.Sprintf(\"%s|%s|%d|%s\", s.accountInfo(), a, s.ExpiresOn.Unix(), r), nil\n}\n\nfunc decodeSessionStatePlain(v string) (s *SessionState, err error) {\n\tchunks := strings.Split(v, \" \")\n\tif len(chunks) != 2 {\n\t\treturn nil, fmt.Errorf(\"could not decode session state: expected 2 chunks got %d\", len(chunks))\n\t}\n\n\temail := strings.TrimPrefix(chunks[0], \"email:\")\n\tuser := strings.TrimPrefix(chunks[1], \"user:\")\n\tif user == \"\" {\n\t\tuser = strings.Split(email, \"@\")[0]\n\t}\n\n\treturn &SessionState{User: user, Email: email}, nil\n}\n\nfunc DecodeSessionState(v string, c *cookie.Cipher) (s *SessionState, err error) {\n\tif c == nil {\n\t\treturn decodeSessionStatePlain(v)\n\t}\n\n\tchunks := strings.Split(v, \"|\")\n\tif len(chunks) != 4 {\n\t\terr = fmt.Errorf(\"invalid number of fields (got %d expected 4)\", len(chunks))\n\t\treturn\n\t}\n\n\tsessionState, err := decodeSessionStatePlain(chunks[0])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif chunks[1] != \"\" {\n\t\tif sessionState.AccessToken, err = c.Decrypt(chunks[1]); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tts, _ := strconv.Atoi(chunks[2])\n\tsessionState.ExpiresOn = time.Unix(int64(ts), 0)\n\n\tif chunks[3] != \"\" {\n\t\tif sessionState.RefreshToken, err = c.Decrypt(chunks[3]); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn sessionState, nil\n}\n"
  },
  {
    "path": "providers/session_state_test.go",
    "content": "package providers\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bitly/oauth2_proxy/cookie\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst secret = \"0123456789abcdefghijklmnopqrstuv\"\nconst altSecret = \"0000000000abcdefghijklmnopqrstuv\"\n\nfunc TestSessionStateSerialization(t *testing.T) {\n\tc, err := cookie.NewCipher([]byte(secret))\n\tassert.Equal(t, nil, err)\n\tc2, err := cookie.NewCipher([]byte(altSecret))\n\tassert.Equal(t, nil, err)\n\ts := &SessionState{\n\t\tEmail:        \"user@domain.com\",\n\t\tAccessToken:  \"token1234\",\n\t\tExpiresOn:    time.Now().Add(time.Duration(1) * time.Hour),\n\t\tRefreshToken: \"refresh4321\",\n\t}\n\tencoded, err := s.EncodeSessionState(c)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 3, strings.Count(encoded, \"|\"))\n\n\tss, err := DecodeSessionState(encoded, c)\n\tt.Logf(\"%#v\", ss)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, \"user\", ss.User)\n\tassert.Equal(t, s.Email, ss.Email)\n\tassert.Equal(t, s.AccessToken, ss.AccessToken)\n\tassert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())\n\tassert.Equal(t, s.RefreshToken, ss.RefreshToken)\n\n\t// ensure a different cipher can't decode properly (ie: it gets gibberish)\n\tss, err = DecodeSessionState(encoded, c2)\n\tt.Logf(\"%#v\", ss)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, \"user\", ss.User)\n\tassert.Equal(t, s.Email, ss.Email)\n\tassert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())\n\tassert.NotEqual(t, s.AccessToken, ss.AccessToken)\n\tassert.NotEqual(t, s.RefreshToken, ss.RefreshToken)\n}\n\nfunc TestSessionStateSerializationWithUser(t *testing.T) {\n\tc, err := cookie.NewCipher([]byte(secret))\n\tassert.Equal(t, nil, err)\n\tc2, err := cookie.NewCipher([]byte(altSecret))\n\tassert.Equal(t, nil, err)\n\ts := &SessionState{\n\t\tUser:         \"just-user\",\n\t\tEmail:        \"user@domain.com\",\n\t\tAccessToken:  \"token1234\",\n\t\tExpiresOn:    time.Now().Add(time.Duration(1) * time.Hour),\n\t\tRefreshToken: \"refresh4321\",\n\t}\n\tencoded, err := s.EncodeSessionState(c)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 3, strings.Count(encoded, \"|\"))\n\n\tss, err := DecodeSessionState(encoded, c)\n\tt.Logf(\"%#v\", ss)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, s.User, ss.User)\n\tassert.Equal(t, s.Email, ss.Email)\n\tassert.Equal(t, s.AccessToken, ss.AccessToken)\n\tassert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())\n\tassert.Equal(t, s.RefreshToken, ss.RefreshToken)\n\n\t// ensure a different cipher can't decode properly (ie: it gets gibberish)\n\tss, err = DecodeSessionState(encoded, c2)\n\tt.Logf(\"%#v\", ss)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, s.User, ss.User)\n\tassert.Equal(t, s.Email, ss.Email)\n\tassert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())\n\tassert.NotEqual(t, s.AccessToken, ss.AccessToken)\n\tassert.NotEqual(t, s.RefreshToken, ss.RefreshToken)\n}\n\nfunc TestSessionStateSerializationNoCipher(t *testing.T) {\n\ts := &SessionState{\n\t\tEmail:        \"user@domain.com\",\n\t\tAccessToken:  \"token1234\",\n\t\tExpiresOn:    time.Now().Add(time.Duration(1) * time.Hour),\n\t\tRefreshToken: \"refresh4321\",\n\t}\n\tencoded, err := s.EncodeSessionState(nil)\n\tassert.Equal(t, nil, err)\n\texpected := fmt.Sprintf(\"email:%s user:\", s.Email)\n\tassert.Equal(t, expected, encoded)\n\n\t// only email should have been serialized\n\tss, err := DecodeSessionState(encoded, nil)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, \"user\", ss.User)\n\tassert.Equal(t, s.Email, ss.Email)\n\tassert.Equal(t, \"\", ss.AccessToken)\n\tassert.Equal(t, \"\", ss.RefreshToken)\n}\n\nfunc TestSessionStateSerializationNoCipherWithUser(t *testing.T) {\n\ts := &SessionState{\n\t\tUser:         \"just-user\",\n\t\tEmail:        \"user@domain.com\",\n\t\tAccessToken:  \"token1234\",\n\t\tExpiresOn:    time.Now().Add(time.Duration(1) * time.Hour),\n\t\tRefreshToken: \"refresh4321\",\n\t}\n\tencoded, err := s.EncodeSessionState(nil)\n\tassert.Equal(t, nil, err)\n\texpected := fmt.Sprintf(\"email:%s user:%s\", s.Email, s.User)\n\tassert.Equal(t, expected, encoded)\n\n\t// only email should have been serialized\n\tss, err := DecodeSessionState(encoded, nil)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, s.User, ss.User)\n\tassert.Equal(t, s.Email, ss.Email)\n\tassert.Equal(t, \"\", ss.AccessToken)\n\tassert.Equal(t, \"\", ss.RefreshToken)\n}\n\nfunc TestSessionStateAccountInfo(t *testing.T) {\n\ts := &SessionState{\n\t\tEmail: \"user@domain.com\",\n\t\tUser:  \"just-user\",\n\t}\n\texpected := fmt.Sprintf(\"email:%v user:%v\", s.Email, s.User)\n\tassert.Equal(t, expected, s.accountInfo())\n\n\ts.Email = \"\"\n\texpected = fmt.Sprintf(\"email:%v user:%v\", s.Email, s.User)\n\tassert.Equal(t, expected, s.accountInfo())\n}\n\nfunc TestExpired(t *testing.T) {\n\ts := &SessionState{ExpiresOn: time.Now().Add(time.Duration(-1) * time.Minute)}\n\tassert.Equal(t, true, s.IsExpired())\n\n\ts = &SessionState{ExpiresOn: time.Now().Add(time.Duration(1) * time.Minute)}\n\tassert.Equal(t, false, s.IsExpired())\n\n\ts = &SessionState{}\n\tassert.Equal(t, false, s.IsExpired())\n}\n"
  },
  {
    "path": "string_array.go",
    "content": "package main\n\nimport (\n\t\"strings\"\n)\n\ntype StringArray []string\n\nfunc (a *StringArray) Set(s string) error {\n\t*a = append(*a, s)\n\treturn nil\n}\n\nfunc (a *StringArray) String() string {\n\treturn strings.Join(*a, \",\")\n}\n"
  },
  {
    "path": "templates.go",
    "content": "package main\n\nimport (\n\t\"html/template\"\n\t\"log\"\n\t\"path\"\n)\n\nfunc loadTemplates(dir string) *template.Template {\n\tif dir == \"\" {\n\t\treturn getTemplates()\n\t}\n\tlog.Printf(\"using custom template directory %q\", dir)\n\tt, err := template.New(\"\").ParseFiles(path.Join(dir, \"sign_in.html\"), path.Join(dir, \"error.html\"))\n\tif err != nil {\n\t\tlog.Fatalf(\"failed parsing template %s\", err)\n\t}\n\treturn t\n}\n\nfunc getTemplates() *template.Template {\n\tt, err := template.New(\"foo\").Parse(`{{define \"sign_in.html\"}}\n<!DOCTYPE html>\n<html lang=\"en\" charset=\"utf-8\">\n<head>\n\t<title>Sign In</title>\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n\t<style>\n\tbody {\n\t\tfont-family: \"Helvetica Neue\",Helvetica,Arial,sans-serif;\n\t\tfont-size: 14px;\n\t\tline-height: 1.42857143;\n\t\tcolor: #333;\n\t\tbackground: #f0f0f0;\n\t}\n\t.signin {\n\t\tdisplay:block;\n\t\tmargin:20px auto;\n\t\tmax-width:400px;\n\t\tbackground: #fff;\n\t\tborder:1px solid #ccc;\n\t\tborder-radius: 10px;\n\t\tpadding: 20px;\n\t}\n\t.center {\n\t\ttext-align:center;\n\t}\n\t.btn {\n\t\tcolor: #fff;\n\t\tbackground-color: #428bca;\n\t\tborder: 1px solid #357ebd;\n\t\t-webkit-border-radius: 4;\n\t\t-moz-border-radius: 4;\n\t\tborder-radius: 4px;\n\t\tfont-size: 14px;\n\t\tpadding: 6px 12px;\n\t  \ttext-decoration: none;\n\t\tcursor: pointer;\n\t}\n\n\t.btn:hover {\n\t\tbackground-color: #3071a9;\n\t\tborder-color: #285e8e;\n\t\ttext-decoration: none;\n\t}\n\tlabel {\n\t\tdisplay: inline-block;\n\t\tmax-width: 100%;\n\t\tmargin-bottom: 5px;\n\t\tfont-weight: 700;\n\t}\n\tinput {\n\t\tdisplay: block;\n\t\twidth: 100%;\n\t\theight: 34px;\n\t\tpadding: 6px 12px;\n\t\tfont-size: 14px;\n\t\tline-height: 1.42857143;\n\t\tcolor: #555;\n\t\tbackground-color: #fff;\n\t\tbackground-image: none;\n\t\tborder: 1px solid #ccc;\n\t\tborder-radius: 4px;\n\t\t-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);\n\t\tbox-shadow: inset 0 1px 1px rgba(0,0,0,.075);\n\t\t-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;\n\t\t-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;\n\t\ttransition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;\n\t\tmargin:0;\n\t\tbox-sizing: border-box;\n\t}\n\tfooter {\n\t\tdisplay:block;\n\t\tfont-size:10px;\n\t\tcolor:#aaa;\n\t\ttext-align:center;\n\t\tmargin-bottom:10px;\n\t}\n\tfooter a {\n\t\tdisplay:inline-block;\n\t\theight:25px;\n\t\tline-height:25px;\n\t\tcolor:#aaa;\n\t\ttext-decoration:underline;\n\t}\n\tfooter a:hover {\n\t\tcolor:#aaa;\n\t}\n\t</style>\n</head>\n<body>\n\t<div class=\"signin center\">\n\t<form method=\"GET\" action=\"{{.ProxyPrefix}}/start\">\n\t<input type=\"hidden\" name=\"rd\" value=\"{{.Redirect}}\">\n\t{{ if .SignInMessage }}\n\t<p>{{.SignInMessage}}</p>\n\t{{ end}}\n\t<button type=\"submit\" class=\"btn\">Sign in with {{.ProviderName}}</button><br/>\n\t</form>\n\t</div>\n\n\t{{ if .CustomLogin }}\n\t<div class=\"signin\">\n\t<form method=\"POST\" action=\"{{.ProxyPrefix}}/sign_in\">\n\t\t<input type=\"hidden\" name=\"rd\" value=\"{{.Redirect}}\">\n\t\t<label for=\"username\">Username:</label><input type=\"text\" name=\"username\" id=\"username\" size=\"10\"><br/>\n\t\t<label for=\"password\">Password:</label><input type=\"password\" name=\"password\" id=\"password\" size=\"10\"><br/>\n\t\t<button type=\"submit\" class=\"btn\">Sign In</button>\n\t</form>\n\t</div>\n\t{{ end }}\n\t<script>\n\t\tif (window.location.hash) {\n\t\t\t(function() {\n\t\t\t\tvar inputs = document.getElementsByName('rd');\n\t\t\t\tfor (var i = 0; i < inputs.length; i++) {\n\t\t\t\t\tinputs[i].value += window.location.hash;\n\t\t\t\t}\n\t\t\t})();\n\t\t}\n\t</script>\n\t<footer>\n\t{{ if eq .Footer \"-\" }}\n\t{{ else if eq .Footer \"\"}}\n\tSecured with <a href=\"https://github.com/bitly/oauth2_proxy#oauth2_proxy\">OAuth2 Proxy</a> version {{.Version}}\n\t{{ else }}\n\t{{.Footer}}\n\t{{ end }}\n\t</footer>\n</body>\n</html>\n{{end}}`)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed parsing template %s\", err)\n\t}\n\n\tt, err = t.Parse(`{{define \"error.html\"}}\n<!DOCTYPE html>\n<html lang=\"en\" charset=\"utf-8\">\n<head>\n\t<title>{{.Title}}</title>\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n</head>\n<body>\n\t<h2>{{.Title}}</h2>\n\t<p>{{.Message}}</p>\n\t<hr>\n\t<p><a href=\"{{.ProxyPrefix}}/sign_in\">Sign In</a></p>\n</body>\n</html>{{end}}`)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed parsing template %s\", err)\n\t}\n\treturn t\n}\n"
  },
  {
    "path": "templates_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTemplatesCompile(t *testing.T) {\n\ttemplates := getTemplates()\n\tassert.NotEqual(t, templates, nil)\n}\n"
  },
  {
    "path": "test.sh",
    "content": "#!/bin/bash\nEXIT_CODE=0\necho \"gofmt\"\ndiff -u <(echo -n) <(gofmt -d $(find . -type f -name '*.go' -not -path \"./vendor/*\")) || EXIT_CODE=1\nfor pkg in $(go list ./... | grep -v '/vendor/' ); do\n    echo \"testing $pkg\"\n    echo \"go vet $pkg\"\n    go vet \"$pkg\" || EXIT_CODE=1\n    echo \"go test -v $pkg\"\n    go test -v -timeout 90s \"$pkg\" || EXIT_CODE=1\n    echo \"go test -v -race $pkg\"\n    GOMAXPROCS=4 go test -v -timeout 90s0s -race \"$pkg\" || EXIT_CODE=1\ndone\nexit $EXIT_CODE"
  },
  {
    "path": "validator.go",
    "content": "package main\n\nimport (\n\t\"encoding/csv\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"unsafe\"\n)\n\ntype UserMap struct {\n\tusersFile string\n\tm         unsafe.Pointer\n}\n\nfunc NewUserMap(usersFile string, done <-chan bool, onUpdate func()) *UserMap {\n\tum := &UserMap{usersFile: usersFile}\n\tm := make(map[string]bool)\n\tatomic.StorePointer(&um.m, unsafe.Pointer(&m))\n\tif usersFile != \"\" {\n\t\tlog.Printf(\"using authenticated emails file %s\", usersFile)\n\t\tWatchForUpdates(usersFile, done, func() {\n\t\t\tum.LoadAuthenticatedEmailsFile()\n\t\t\tonUpdate()\n\t\t})\n\t\tum.LoadAuthenticatedEmailsFile()\n\t}\n\treturn um\n}\n\nfunc (um *UserMap) IsValid(email string) (result bool) {\n\tm := *(*map[string]bool)(atomic.LoadPointer(&um.m))\n\t_, result = m[email]\n\treturn\n}\n\nfunc (um *UserMap) LoadAuthenticatedEmailsFile() {\n\tr, err := os.Open(um.usersFile)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed opening authenticated-emails-file=%q, %s\", um.usersFile, err)\n\t}\n\tdefer r.Close()\n\tcsv_reader := csv.NewReader(r)\n\tcsv_reader.Comma = ','\n\tcsv_reader.Comment = '#'\n\tcsv_reader.TrimLeadingSpace = true\n\trecords, err := csv_reader.ReadAll()\n\tif err != nil {\n\t\tlog.Printf(\"error reading authenticated-emails-file=%q, %s\", um.usersFile, err)\n\t\treturn\n\t}\n\tupdated := make(map[string]bool)\n\tfor _, r := range records {\n\t\taddress := strings.ToLower(strings.TrimSpace(r[0]))\n\t\tupdated[address] = true\n\t}\n\tatomic.StorePointer(&um.m, unsafe.Pointer(&updated))\n}\n\nfunc newValidatorImpl(domains []string, usersFile string,\n\tdone <-chan bool, onUpdate func()) func(string) bool {\n\tvalidUsers := NewUserMap(usersFile, done, onUpdate)\n\n\tvar allowAll bool\n\tfor i, domain := range domains {\n\t\tif domain == \"*\" {\n\t\t\tallowAll = true\n\t\t\tcontinue\n\t\t}\n\t\tdomains[i] = fmt.Sprintf(\"@%s\", strings.ToLower(domain))\n\t}\n\n\tvalidator := func(email string) (valid bool) {\n\t\tif email == \"\" {\n\t\t\treturn\n\t\t}\n\t\temail = strings.ToLower(email)\n\t\tfor _, domain := range domains {\n\t\t\tvalid = valid || strings.HasSuffix(email, domain)\n\t\t}\n\t\tif !valid {\n\t\t\tvalid = validUsers.IsValid(email)\n\t\t}\n\t\tif allowAll {\n\t\t\tvalid = true\n\t\t}\n\t\treturn valid\n\t}\n\treturn validator\n}\n\nfunc NewValidator(domains []string, usersFile string) func(string) bool {\n\treturn newValidatorImpl(domains, usersFile, nil, func() {})\n}\n"
  },
  {
    "path": "validator_test.go",
    "content": "package main\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n)\n\ntype ValidatorTest struct {\n\tauth_email_file *os.File\n\tdone            chan bool\n\tupdate_seen     bool\n}\n\nfunc NewValidatorTest(t *testing.T) *ValidatorTest {\n\tvt := &ValidatorTest{}\n\tvar err error\n\tvt.auth_email_file, err = ioutil.TempFile(\"\", \"test_auth_emails_\")\n\tif err != nil {\n\t\tt.Fatal(\"failed to create temp file: \" + err.Error())\n\t}\n\tvt.done = make(chan bool, 1)\n\treturn vt\n}\n\nfunc (vt *ValidatorTest) TearDown() {\n\tvt.done <- true\n\tos.Remove(vt.auth_email_file.Name())\n}\n\nfunc (vt *ValidatorTest) NewValidator(domains []string,\n\tupdated chan<- bool) func(string) bool {\n\treturn newValidatorImpl(domains, vt.auth_email_file.Name(),\n\t\tvt.done, func() {\n\t\t\tif vt.update_seen == false {\n\t\t\t\tupdated <- true\n\t\t\t\tvt.update_seen = true\n\t\t\t}\n\t\t})\n}\n\n// This will close vt.auth_email_file.\nfunc (vt *ValidatorTest) WriteEmails(t *testing.T, emails []string) {\n\tdefer vt.auth_email_file.Close()\n\tvt.auth_email_file.WriteString(strings.Join(emails, \"\\n\"))\n\tif err := vt.auth_email_file.Close(); err != nil {\n\t\tt.Fatal(\"failed to close temp file \" +\n\t\t\tvt.auth_email_file.Name() + \": \" + err.Error())\n\t}\n}\n\nfunc TestValidatorEmpty(t *testing.T) {\n\tvt := NewValidatorTest(t)\n\tdefer vt.TearDown()\n\n\tvt.WriteEmails(t, []string(nil))\n\tdomains := []string(nil)\n\tvalidator := vt.NewValidator(domains, nil)\n\n\tif validator(\"foo.bar@example.com\") {\n\t\tt.Error(\"nothing should validate when the email and \" +\n\t\t\t\"domain lists are empty\")\n\t}\n}\n\nfunc TestValidatorSingleEmail(t *testing.T) {\n\tvt := NewValidatorTest(t)\n\tdefer vt.TearDown()\n\n\tvt.WriteEmails(t, []string{\"foo.bar@example.com\"})\n\tdomains := []string(nil)\n\tvalidator := vt.NewValidator(domains, nil)\n\n\tif !validator(\"foo.bar@example.com\") {\n\t\tt.Error(\"email should validate\")\n\t}\n\tif validator(\"baz.quux@example.com\") {\n\t\tt.Error(\"email from same domain but not in list \" +\n\t\t\t\"should not validate when domain list is empty\")\n\t}\n}\n\nfunc TestValidatorSingleDomain(t *testing.T) {\n\tvt := NewValidatorTest(t)\n\tdefer vt.TearDown()\n\n\tvt.WriteEmails(t, []string(nil))\n\tdomains := []string{\"example.com\"}\n\tvalidator := vt.NewValidator(domains, nil)\n\n\tif !validator(\"foo.bar@example.com\") {\n\t\tt.Error(\"email should validate\")\n\t}\n\tif !validator(\"baz.quux@example.com\") {\n\t\tt.Error(\"email from same domain should validate\")\n\t}\n}\n\nfunc TestValidatorMultipleEmailsMultipleDomains(t *testing.T) {\n\tvt := NewValidatorTest(t)\n\tdefer vt.TearDown()\n\n\tvt.WriteEmails(t, []string{\n\t\t\"xyzzy@example.com\",\n\t\t\"plugh@example.com\",\n\t})\n\tdomains := []string{\"example0.com\", \"example1.com\"}\n\tvalidator := vt.NewValidator(domains, nil)\n\n\tif !validator(\"foo.bar@example0.com\") {\n\t\tt.Error(\"email from first domain should validate\")\n\t}\n\tif !validator(\"baz.quux@example1.com\") {\n\t\tt.Error(\"email from second domain should validate\")\n\t}\n\tif !validator(\"xyzzy@example.com\") {\n\t\tt.Error(\"first email in list should validate\")\n\t}\n\tif !validator(\"plugh@example.com\") {\n\t\tt.Error(\"second email in list should validate\")\n\t}\n\tif validator(\"xyzzy.plugh@example.com\") {\n\t\tt.Error(\"email not in list that matches no domains \" +\n\t\t\t\"should not validate\")\n\t}\n}\n\nfunc TestValidatorComparisonsAreCaseInsensitive(t *testing.T) {\n\tvt := NewValidatorTest(t)\n\tdefer vt.TearDown()\n\n\tvt.WriteEmails(t, []string{\"Foo.Bar@Example.Com\"})\n\tdomains := []string{\"Frobozz.Com\"}\n\tvalidator := vt.NewValidator(domains, nil)\n\n\tif !validator(\"foo.bar@example.com\") {\n\t\tt.Error(\"loaded email addresses are not lower-cased\")\n\t}\n\tif !validator(\"Foo.Bar@Example.Com\") {\n\t\tt.Error(\"validated email addresses are not lower-cased\")\n\t}\n\tif !validator(\"foo.bar@frobozz.com\") {\n\t\tt.Error(\"loaded domains are not lower-cased\")\n\t}\n\tif !validator(\"foo.bar@Frobozz.Com\") {\n\t\tt.Error(\"validated domains are not lower-cased\")\n\t}\n}\n\nfunc TestValidatorIgnoreSpacesInAuthEmails(t *testing.T) {\n\tvt := NewValidatorTest(t)\n\tdefer vt.TearDown()\n\n\tvt.WriteEmails(t, []string{\"   foo.bar@example.com   \"})\n\tdomains := []string(nil)\n\tvalidator := vt.NewValidator(domains, nil)\n\n\tif !validator(\"foo.bar@example.com\") {\n\t\tt.Error(\"email should validate\")\n\t}\n}\n"
  },
  {
    "path": "validator_watcher_copy_test.go",
    "content": "// +build go1.3,!plan9,!solaris,!windows\n\n// Turns out you can't copy over an existing file on Windows.\n\npackage main\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"testing\"\n)\n\nfunc (vt *ValidatorTest) UpdateEmailFileViaCopyingOver(\n\tt *testing.T, emails []string) {\n\torig_file := vt.auth_email_file\n\tvar err error\n\tvt.auth_email_file, err = ioutil.TempFile(\"\", \"test_auth_emails_\")\n\tif err != nil {\n\t\tt.Fatal(\"failed to create temp file for copy: \" + err.Error())\n\t}\n\tvt.WriteEmails(t, emails)\n\terr = os.Rename(vt.auth_email_file.Name(), orig_file.Name())\n\tif err != nil {\n\t\tt.Fatal(\"failed to copy over temp file: \" + err.Error())\n\t}\n\tvt.auth_email_file = orig_file\n}\n\nfunc TestValidatorOverwriteEmailListViaCopyingOver(t *testing.T) {\n\tvt := NewValidatorTest(t)\n\tdefer vt.TearDown()\n\n\tvt.WriteEmails(t, []string{\"xyzzy@example.com\"})\n\tdomains := []string(nil)\n\tupdated := make(chan bool)\n\tvalidator := vt.NewValidator(domains, updated)\n\n\tif !validator(\"xyzzy@example.com\") {\n\t\tt.Error(\"email in list should validate\")\n\t}\n\n\tvt.UpdateEmailFileViaCopyingOver(t, []string{\"plugh@example.com\"})\n\t<-updated\n\n\tif validator(\"xyzzy@example.com\") {\n\t\tt.Error(\"email removed from list should not validate\")\n\t}\n}\n"
  },
  {
    "path": "validator_watcher_test.go",
    "content": "// +build go1.3,!plan9,!solaris\n\npackage main\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"testing\"\n)\n\nfunc (vt *ValidatorTest) UpdateEmailFile(t *testing.T, emails []string) {\n\tvar err error\n\tvt.auth_email_file, err = os.OpenFile(\n\t\tvt.auth_email_file.Name(), os.O_WRONLY|os.O_CREATE, 0600)\n\tif err != nil {\n\t\tt.Fatal(\"failed to re-open temp file for updates\")\n\t}\n\tvt.WriteEmails(t, emails)\n}\n\nfunc (vt *ValidatorTest) UpdateEmailFileViaRenameAndReplace(\n\tt *testing.T, emails []string) {\n\torig_file := vt.auth_email_file\n\tvar err error\n\tvt.auth_email_file, err = ioutil.TempFile(\"\", \"test_auth_emails_\")\n\tif err != nil {\n\t\tt.Fatal(\"failed to create temp file for rename and replace: \" +\n\t\t\terr.Error())\n\t}\n\tvt.WriteEmails(t, emails)\n\n\tmoved_name := orig_file.Name() + \"-moved\"\n\terr = os.Rename(orig_file.Name(), moved_name)\n\terr = os.Rename(vt.auth_email_file.Name(), orig_file.Name())\n\tif err != nil {\n\t\tt.Fatal(\"failed to rename and replace temp file: \" +\n\t\t\terr.Error())\n\t}\n\tvt.auth_email_file = orig_file\n\tos.Remove(moved_name)\n}\n\nfunc TestValidatorOverwriteEmailListDirectly(t *testing.T) {\n\tvt := NewValidatorTest(t)\n\tdefer vt.TearDown()\n\n\tvt.WriteEmails(t, []string{\n\t\t\"xyzzy@example.com\",\n\t\t\"plugh@example.com\",\n\t})\n\tdomains := []string(nil)\n\tupdated := make(chan bool)\n\tvalidator := vt.NewValidator(domains, updated)\n\n\tif !validator(\"xyzzy@example.com\") {\n\t\tt.Error(\"first email in list should validate\")\n\t}\n\tif !validator(\"plugh@example.com\") {\n\t\tt.Error(\"second email in list should validate\")\n\t}\n\tif validator(\"xyzzy.plugh@example.com\") {\n\t\tt.Error(\"email not in list that matches no domains \" +\n\t\t\t\"should not validate\")\n\t}\n\n\tvt.UpdateEmailFile(t, []string{\n\t\t\"xyzzy.plugh@example.com\",\n\t\t\"plugh@example.com\",\n\t})\n\t<-updated\n\n\tif validator(\"xyzzy@example.com\") {\n\t\tt.Error(\"email removed from list should not validate\")\n\t}\n\tif !validator(\"plugh@example.com\") {\n\t\tt.Error(\"email retained in list should validate\")\n\t}\n\tif !validator(\"xyzzy.plugh@example.com\") {\n\t\tt.Error(\"email added to list should validate\")\n\t}\n}\n\nfunc TestValidatorOverwriteEmailListViaRenameAndReplace(t *testing.T) {\n\tvt := NewValidatorTest(t)\n\tdefer vt.TearDown()\n\n\tvt.WriteEmails(t, []string{\"xyzzy@example.com\"})\n\tdomains := []string(nil)\n\tupdated := make(chan bool, 1)\n\tvalidator := vt.NewValidator(domains, updated)\n\n\tif !validator(\"xyzzy@example.com\") {\n\t\tt.Error(\"email in list should validate\")\n\t}\n\n\tvt.UpdateEmailFileViaRenameAndReplace(t, []string{\"plugh@example.com\"})\n\t<-updated\n\n\tif validator(\"xyzzy@example.com\") {\n\t\tt.Error(\"email removed from list should not validate\")\n\t}\n}\n"
  },
  {
    "path": "version.go",
    "content": "package main\n\nconst VERSION = \"2.2.1-alpha\"\n"
  },
  {
    "path": "watcher.go",
    "content": "// +build go1.3,!plan9,!solaris\n\npackage main\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"gopkg.in/fsnotify.v1\"\n)\n\nfunc WaitForReplacement(filename string, op fsnotify.Op,\n\twatcher *fsnotify.Watcher) {\n\tconst sleep_interval = 50 * time.Millisecond\n\n\t// Avoid a race when fsnofity.Remove is preceded by fsnotify.Chmod.\n\tif op&fsnotify.Chmod != 0 {\n\t\ttime.Sleep(sleep_interval)\n\t}\n\tfor {\n\t\tif _, err := os.Stat(filename); err == nil {\n\t\t\tif err := watcher.Add(filename); err == nil {\n\t\t\t\tlog.Printf(\"watching resumed for %s\", filename)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\ttime.Sleep(sleep_interval)\n\t}\n}\n\nfunc WatchForUpdates(filename string, done <-chan bool, action func()) {\n\tfilename = filepath.Clean(filename)\n\twatcher, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\tlog.Fatal(\"failed to create watcher for \", filename, \": \", err)\n\t}\n\tgo func() {\n\t\tdefer watcher.Close()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase _ = <-done:\n\t\t\t\tlog.Printf(\"Shutting down watcher for: %s\", filename)\n\t\t\t\tbreak\n\t\t\tcase event := <-watcher.Events:\n\t\t\t\t// On Arch Linux, it appears Chmod events precede Remove events,\n\t\t\t\t// which causes a race between action() and the coming Remove event.\n\t\t\t\t// If the Remove wins, the action() (which calls\n\t\t\t\t// UserMap.LoadAuthenticatedEmailsFile()) crashes when the file\n\t\t\t\t// can't be opened.\n\t\t\t\tif event.Op&(fsnotify.Remove|fsnotify.Rename|fsnotify.Chmod) != 0 {\n\t\t\t\t\tlog.Printf(\"watching interrupted on event: %s\", event)\n\t\t\t\t\twatcher.Remove(filename)\n\t\t\t\t\tWaitForReplacement(filename, event.Op, watcher)\n\t\t\t\t}\n\t\t\t\tlog.Printf(\"reloading after event: %s\", event)\n\t\t\t\taction()\n\t\t\tcase err := <-watcher.Errors:\n\t\t\t\tlog.Printf(\"error watching %s: %s\", filename, err)\n\t\t\t}\n\t\t}\n\t}()\n\tif err = watcher.Add(filename); err != nil {\n\t\tlog.Fatal(\"failed to add \", filename, \" to watcher: \", err)\n\t}\n\tlog.Printf(\"watching %s for updates\", filename)\n}\n"
  },
  {
    "path": "watcher_unsupported.go",
    "content": "// +build !go1.3 plan9 solaris\n\npackage main\n\nimport (\n\t\"log\"\n)\n\nfunc WatchForUpdates(filename string, done <-chan bool, action func()) {\n\tlog.Printf(\"file watching not implemented on this platform\")\n\tgo func() { <-done }()\n}\n"
  }
]