Programmatic access

This page describes how to obtain Pomerium access credentials programmatically via a web-based oauth2 based authorization flow. If you have ever used Google's gcloud commandline app, the mechanism is very similar.

Components

Login API

The API returns a signed, sign-in url that can be used to complete a user-driven login process with Pomerium and your identity provider. The Login API endpoints takes a redirect_uri query param as an argument which points to the location of the callback server to be called following a successful login.

For example:

$ curl "https://httpbin.example.com/.pomerium/api/v1/login?redirect_uri=http://localhost:8000"

https://authenticate.example.com/.pomerium/sign_in?redirect_uri=http%3A%2F%2Flocalhost%3Fpomerium_callback_uri%3Dhttps%253A%252F%252Fhttpbin.corp.example%252F.pomerium%252Fapi%252Fv1%252Flogin%253Fredirect_uri%253Dhttp%253A%252F%252Flocalhost&sig=hsLuzJctmgsN4kbMeQL16fe_FahjDBEcX0_kPYfg8bs%3D&ts=1573262981

Callback handler

It is the script or application's responsibility to create a HTTP callback handler. Authenticated sessions are returned in the form of a callback from pomerium to a HTTP server. This is the redirect_uri value used to build Login API's URL, and represents the URL of a (usually local) http server responsible for receiving the resulting user session in the form of pomerium_jwt and pomerium_refresh_token query parameters.

See the python script below for example of how to start a callback server, and store the session payload.

Refresh API

The Refresh API allows for a valid refresh token enabled session, using an Authorization: Pomerium bearer token, to refresh the current user session and return a new user session (jwt) and refresh token (refresh_token). If successfully, a new updated refresh token and identity session are returned as a json response.

$ curl \
	-H "Accept: application/json" \
	-H "Authorization: Pomerium $(cat cred-from-above-step.json | jq -r .refresh_token)" \
	https://authenticate.example.com/api/v1/refresh

{
  "jwt":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token":"fXiWCF_z1NWKU3yZ...."
}

Note that the Authorization refresh token is set to Authorization Pomerium not Bearer.

Handling expiration and revocation

Your application should handle token expiration. If the session expires before work is done, the identity provider issued refresh_token can be used to create a new valid session.

Also, your script or application should anticipate the possibility that a granted refresh_token may stop working. For example, a refresh token might stop working if the underlying user changes passwords, revokes access, or if the administrator removes rotates or deletes the OAuth Client ID.

High level workflow

The application interacting with Pomerium must manage the following workflow. Consider the following example where a script or program desires delegated, programmatic access to the domain httpbin.corp.domain.example:

  1. The script or application requests a new login url from the pomerium managed endpoint (e.g. https://httpbin.corp.domain.example/.pomerium/api/v1/login) and takes a redirect_uri as an argument.
  2. The script or application opens a browser or redirects the user to the returned login page.
  3. The user completes the identity providers login flow.
  4. The identity provider makes a callback to pomerium's authenticate service (e.g. authenticate.corp.domain.example) .
  5. Pomerium's authenticate service creates a user session and redirect token, then redirects back to the the managed endpoint (e.g. httpbin.corp.domain.example)
  6. Pomerium's proxy service and makes a callback request to the original redirect_uri with the user session and refresh token as arguments.
  7. The script or application is responsible for handling that http callback request, and securely handling the callback session (pomerium_jwt) and refresh token (pomerium_refresh_token) queryparams.
  8. The script or application can now make any requests as normal, by setting the Authorization: Pomerium ${pomerium_jwt} header.
  9. If the script or application encounters a 401 error or token expiration error, the script or application can make a request the authenticate service's refresh api endpoint (e.g. https://authenticate.corp.domain.example/api/v1/refresh) with the Authorization: Pomerium ${pomerium_refresh_token} header. Note that the refresh token is used, not the user session jwt. If successful, a new user session jwt and refresh token will be returned and requests can continue as before.

Example Code

Please consider see the following minimal but complete python example.

python3 scripts/programmatic_access.py \
	--dst https://httpbin.example.com/headers \
	--refresh-endpoint https://authenticate.example.com/api/v1/refresh
from __future__ import absolute_import, division, print_function

import argparse
import http.server
import json
import sys
import urllib.parse
import webbrowser
from urllib.parse import urlparse
import requests

done = False

parser = argparse.ArgumentParser()
parser.add_argument("--login", action="store_true")
parser.add_argument(
    "--dst", default="https://httpbin.example.com/headers",
)
parser.add_argument(
    "--refresh-endpoint", default="https://authenticate.example.com/api/v1/refresh",
)
parser.add_argument("--server", default="localhost", type=str)
parser.add_argument("--port", default=8000, type=int)
parser.add_argument(
    "--cred", default="pomerium-cred.json",
)
args = parser.parse_args()


class PomeriumSession:
    def __init__(self, jwt, refresh_token):
        self.jwt = jwt
        self.refresh_token = refresh_token

    def to_json(self):
        return json.dumps(self.__dict__, indent=2)

    @classmethod
    def from_json_file(cls, fn):
        with open(fn) as f:
            data = json.load(f)
            return cls(**data)


class Callback(http.server.BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        # silence http server logs for now
        return

    def do_GET(self):
        global args
        global done
        self.send_response(200)
        self.end_headers()
        response = b"OK"
        if "pomerium" in self.path:
            path = urllib.parse.urlparse(self.path).query
            path_qp = urllib.parse.parse_qs(path)
            session = PomeriumSession(
                path_qp.get("pomerium_jwt")[0],
                path_qp.get("pomerium_refresh_token")[0],
            )
            done = True
            response = session.to_json().encode()
            with open(args.cred, "w", encoding="utf-8") as f:
                f.write(session.to_json())
                print("=> pomerium json credential saved to:\n{}".format(f.name))

        self.wfile.write(response)


def main():
    global args

    dst = urllib.parse.urlparse(args.dst)
    try:
        cred = PomeriumSession.from_json_file(args.cred)
    except:
        print("=> no credential found, let's login")
        args.login = True

    # initial login to make sure we have our credential
    if args.login:
        dst = urllib.parse.urlparse(args.dst)
        query_params = {
            "pomerium_redirect_uri": "http://{}:{}".format(args.server, args.port)
        }
        enc_query_params = urllib.parse.urlencode(query_params)
        dst_login = "{}://{}{}?{}".format(
            dst.scheme, dst.hostname, "/.pomerium/api/v1/login", enc_query_params,
        )
        response = requests.get(dst_login)
        print("=> Your browser has been opened to visit:\n{}".format(response.text))
        webbrowser.open(response.text)

        with http.server.HTTPServer((args.server, args.port), Callback) as httpd:
            while not done:
                httpd.handle_request()

    cred = PomeriumSession.from_json_file(args.cred)
    response = requests.get(
        args.dst,
        headers={
            "Authorization": "Pomerium {}".format(cred.jwt),
            "Content-type": "application/json",
            "Accept": "application/json",
        },
    )
    print(
        "==> request\n{}\n==> response.status_code\n{}\n==>response.text\n{}\n".format(
            args.dst, response.status_code, response.text
        )
    )
    # if response.status_code == 200:
    if response.status_code == 401:
        # user our refresh token to get a new cred
        print("==> got a 401, let's try to refresh that credential")
        response = requests.get(
            args.refresh_endpoint,
            headers={
                "Authorization": "Pomerium {}".format(cred.refresh_token),
                "Content-type": "application/json",
                "Accept": "application/json",
            },
        )
        print(
            "==>request\n{}\n ==> response.status_code\n{}\nresponse.text==>\n{}\n".format(
                args.refresh_endpoint, response.status_code, response.text
            )
        )
        # update our cred!
        with open(args.cred, "w", encoding="utf-8") as f:
            f.write(response.text)
            print("=> pomerium json credential saved to:\n{}".format(f.name))


if __name__ == "__main__":
    main()