OAuth 2.0 Authentication

This module provides support for OAuth 2.0 authentication. It will do initial authentication with the specified OAuth 2.0 provider, and then create separate sessions for the clients using (JWT) tokens in cookies. Whenever a session expires, the client is redirected to the OAuth 2.0 provider for re-authentication. The client will then receive an updated (JWT) token with a new expiration time.

The time to live for the sessions is configurable. It is not possible to refresh or revoke tokens.

Example configuration

The below configuration will make Policy Engine to use OAuth 2.0 authentication with Google on our website https://example.com/. Configuration on the Google end is done by creating an OAuth 2.0 Client ID and adding an Authorized redirect URI to it at https://console.cloud.google.com/apis/credentials/. The Authorized redirect URI must match the URL specified in the callback attribute in the endpoint configuration below.

Endpoint

/var/lib/policy-engine/endpoints/demo.json:

{
    "name": "oauth2-google-demo",
    "host": "example.com",
    "path": "/*",
    "modules": [
        {
            "order": 0,
            "name": "auth-oauth2",
            "general": {
                "callback": "https://example.com/callback",
                "client_id": "99999999999-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com",
                "secret": "replace_with_Client_secret_from_provider",
                "provider": "google",
                "jwt_secret": "replace_with_custom_JWT_signing_secret",
                "ttl": "60m",
                "cookie": "some-cookie-name"
            }
        },
        {
            "order": 40,
            "name": "upstream-simple",
            "default": {
                "url":  "http://127.0.0.1:8080/"
            }
        }
    ]
}

Consumer

Add the consumers you allow on the following format.

/var/lib/policy-engine/consumers/demouser.json:

{
   "name": "demouser",
   "email": "demouser@example.com"
}

VCL

# This VCL example relies on Policy Engine running on localhost:8088
# vmod_goto is used with the upstream module
vcl 4.0;
import std;
import curl;
import goto;

# Default backend definition. Set this to point to your content server.
backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

sub vcl_recv {
    # Unset to clear any headers supplied by client
    unset req.http.location;
    # Prepare curl callout:
    # 1. set method to mirror the request method
    curl.set_method(req.method);
    # 2. add all headers from original request
    curl.header_add_all();
    # 3. remove content-length, as we're not sending the request body
    curl.header_remove("content-length");
    # 4. discard original requests transfer encoding from the callout request
    curl.header_remove("transfer-encoding");

    # Callout to Policy Engine
    # If the original HTTP method was a HEAD, use curl.head
    # Any other HTTP method will result in a GET
    if (req.method == "HEAD") {
        std.log(" ***** Calling policy-engine with curl.head");
        curl.head("http://localhost:8088" + req.url);
    } else {
        std.log(" ***** Calling policy-engine with curl.get");
        curl.get("http://localhost:8088" + req.url);
    }
    std.log(" ***** curl.status is: " + curl.status());

    if (curl.error()) {
        std.log(" ***** curl ERROR: " + curl.error());
    }

    # Reject everything else than a 200 status from Policy Engine
    if (curl.status() != 200) {
        return(synth(curl.status()));
    }

    # Set headers to use for goto
    if (curl.header("x-upstream-url")) {
        set req.http.X-Upstream-Url = curl.header("x-upstream-url");
    }
}

sub vcl_backend_fetch {
    # Use goto if we got info about the upstream from Policy Engine
    if (bereq.http.X-Upstream-Url) {
        set bereq.backend = goto.dns_backend(bereq.http.X-Upstream-Url);
    }
}

sub vcl_deliver {
    call pe_response_headers;
}

sub vcl_synth {
    call pe_response_headers;
}

sub pe_response_headers {
    # This is required to set the JWT cookie
    if (curl.header("set-cookie")) {
        set resp.http.Set-Cookie = curl.header("set-cookie");
    }

    if (curl.header("location")) {
        set resp.http.Location = curl.header("location");
    }

    if (curl.header("x-auth-user")) {
        set resp.http.X-Auth-User = curl.header("x-auth-user");
    }

    if (curl.header("x-auth-type")) {
        set resp.http.X-Auth-Type = curl.header("x-auth-type");
    }

    if (curl.header("x-auth-error")) {
        set resp.http.X-Auth-Error = curl.header("x-auth-error");
    }

}

Authentication and authorization step-by-step

The following is a detailed description of what happens in each of the steps in the authentication and authorization process on a website configured with the example configuration listed above.

  1. A user instructs a browser to send a request to https://example.com/. The user is not authenticated, and the request does not contain the configured cookie (pe_auth_oauth2-google-demo in this example).

  2. When Varnish receives the request, it passes the request on to Policy Engine using vmod curl.

  3. Policy Engine matches the request to the endpoint named oauth2-google-demo in the example configuration above (defined in /var/lib/policy-engine/endpoints/demo.json) since the host and path in the request match the endpoint definition. Relevant excerpt from the endpoint definition:

    {
        "name": "oauth2-google-demo",
        "host": "example.com",
        "path": "/*",
        [...]
    }
    
  4. Policy Engine executes the auth-oauth2 and the upstream-simple modules as specified in the endpoint definition. The auth-oauth2 module has the lowest order, which means that it will be executed first.

  5. The auth-oauth2 module reads the request and looks for a cookie called vpe-oauth2-google-demo. The cookie is unique to each endpoint, however the same cookie name can be used for multiple endpoints. The name of the cookie is derived from the cookie attribute as specified in the endpoint configuration, otherwise it will default to using the endpoint name suffixed by this hardcoded string value: vpe-oauth2-.

    The initial request does not contain the cookie vpe-oauth2-google-demo (or the value of the cookie is invalid or expired), and the user needs to (re-)authenticate using the OAuth 2.0 provider.

  6. Policy Engine responds with a redirect (using a HTTP redirect status code and a Location header) to the OAuth 2.0 provider and a cookie (using a Set-Cookie header). In this case the redirect will point to Google, and the Location header will look something like this: https://accounts.google.com/o/oauth2/auth.... The Set-Cookie header will include a SHA256 signed JWT (JSON Web Token) consisting of a random string (called state), the initial URL path (https://example.com/ in this case) and the endpoint name. The cookie is stored in the browser.

  7. Varnish will detect a non-200 status from Policy Engine in the VCL code block:

    if (curl.status() != 200) {
            return(synth(curl.status()));
    }
    

    Then VCL processing proceeds to vcl_synth where the pe_response_headers sub is called, and the correct request response headers are set based on the headers received from Policy Engine. The important headers that Varnish relays from Policy Engine to the client at this point are Location and Set-Cookie. Relevant excerpt from pe_response_headers:

    if (curl.header("set-cookie")) {
        set resp.http.Set-Cookie = curl.header("set-cookie");
    }
    
    if (curl.header("location")) {
        set resp.http.Location = curl.header("location");
    }
    
  8. The browser sends, according to the redirect response, a request to the Google Accounts OAuth 2.0 endpoint address it received in the Location header.

  9. The request is validated on the Google end, checking that the app is configured to use authentication, and that the callback url is in the whitelist of allowed callback addresses.

  10. On successful validation, Google will present the user with a consent screen, asking if the user wants to use her/his Google account to authenticate with the given Application.

  11. Provided the user consents to this, she/he will get an account selection screen, and if the user is not already logged in be asked to authenticate with Google.

  12. If the user selects a valid Google account, and provides the correct credentials, the OAuth 2.0 endpoint will redirect the user back to the callback address (from /var/lib/policy-engine/endpoints/demo.json), in this case https://example.com/callback, with query parameters state and code (which is the authorization code). The request to the callback endpoint will include the cookie that was set in step 6.

  13. Policy Engine will verify that the value of the state query parameter is the same as the one we initially generated (by comparing it with state in the signed JWT from the cookie).

  14. When the state values have been compared and verified, Policy Engine will reach out to the OAuth 2.0 provider using the authorization code that was provided in step 12. The authorization code will be exchanged for an access token and a refresh token.

  15. The access token will be used to access the provider’s user profile endpoint where Policy Engine will fetch the email address of the user.

  16. Policy Engine will reach out to the OAuth 2.0 provider a last time and revoke the access token and refresh token.

  17. The email address of the authenticated user will now be used for authorization on the specific endpoint. This is done by matching the email address with the consumers and groups that have are allowed to access the endpoint. An authenticated consumer is authorized to use a specific endpoint if either its name or group are set in the endpoint’s configuration.

  18. If the user authorization is successful, Policy Engine will add the consumer name, consumer email and some other properties to the signed JWT which is delivered to the client as a new cookie (using the Set-Cookie header) that will overwrite the cookie that was set in step 6. In addition the browser is redirected (using a HTTP redirect status code and the Location header) to the initial path which in this case is https://example.com/.

  19. At this point a session has been established between the browser and Policy Engine that expires according to the ttl attribute in the endpoint configuration. If none specified it will default to 2 minutes. The JWT in the cookie contains the consumer name, which will be used for authorization throughout the lifetime of the JWT.

    The name of the consumer (specified in /var/lib/policy-engine/consumers/demouser.json for this particular consumer) is available in VCL as STRING curl.header("x-auth-user").

Endpoint configuration attributes

Property Required Type Default Description
provider Yes String The name of the the OAuth2 provider to use (google,github,linkedin).
callback Yes String The URL used as the callback URL. Please note that this URL will be reserved for the OAuth 2.0 process, which means that it can not be used to serve regular content.
client_id Yes String The OAuth2 client id.
secret Yes String The OAuth2 client secret.
jwt_secret No String The secret that is used to sign and verify (JWT) tokens used for client sessions. If none specified, a random string value will be generated.
cookie No String The name of the cookie, which is postfixed by the name of the endpoint.
ttl No String Time to live for each client session after authentication.

Request headers

Header Required Type Description
Cookie Yes String The client must submit a cookie with a valid (JWT) token to get access to a protected resource. The client is redirected to the auth provider if this cookie is not part of the request.

Response status

Status Description
200 The request was authorized to access the specified resource.
307 The request resulted in a redirect.
401 Unauthorized.

Response headers

Header Description
Location Location information used in the Oauth 2.0 auth process.
Set-Cookie Cookie used to pass authentication token.
X-Auth-Error Any human readable authentication error, if present.
X-Auth-Type The auth type used, will with this module be oauth2.
X-Auth-User The username of the authorized user.
X-Auth-Id The id of the authorized user.

®Varnish Software, Wallingatan 12, 111 60 Stockholm, Organization nr. 556805-6203