Programmatic access

This page describes how to access Pomerium endpoints programmatically.

Configuration

Every identity provider has slightly different methods for issuing OAuth 2.0 access tokens suitable for machine-to-machine use, please review your identity provider's documentation. For example:

For the sake of illustration, this guide and example scripts will use Google as the underlying identity provider.

Identity Provider Configuration

To configure programmatic access for Pomerium we'll need to set up an additional OAuth 2.0 client ID that can issue id_tokens whose audience matches the Client ID of Pomerium. Follow these instructions adapted from Google's documentation:

  1. Go to the Credentials page.
  2. Select the project with the Pomerium secured resource.
  3. Click Create credentials, then select OAuth Client ID.
  4. Under Application type, select Other, add a Name, then click Create.
  5. On the OAuth client window that appears, note the client ID and client secret.
  6. On the Credentials window, your new Other credentials appear along with the primary client ID that's used to access your application.

High level flow

The application interacting with Pomerium will roughly have to manage the following access flow.

  1. A user authenticates with the OpenID Connect identity provider. This typically requires handling the Proof Key for Code Exchange process.
  2. Exchange the code from the Proof Key for Code Exchange for a valid refresh_token.
  3. Using the refresh_token from the last step, request the identity provider issue a new id_token which has our Pomerium app's client_id as the audience.
  4. Exchange the identity provider issued id_token for a pomerium token (e.g. https://authenticate.{your-domain}/api/v1/token).
  5. Use the pomerium issued Token authorization bearer token for all requests to Pomerium protected endpoints until it's Expiry. Authorization policy will be tied to the user as normal.

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 by repeating steps 3 and on.

Also, you should write your code to 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.

Example Code

It's not as bad as it sounds. Please see the following minimal but complete examples.

Python

python scripts/programmatic_access.py --client-secret REPLACE_ME \
    --client-id 851877082059-85tfqg9hlm8j9km5d9uripd0dvk72mvk.apps.googleusercontent.com \
    --pomerium-client-id 851877082059-bfgkpj09noog7as3gpc3t7r6n9sjbgs6.apps.googleusercontent.com
from __future__ import absolute_import, division, print_function

import argparse
import json
import sys

import requests

parser = argparse.ArgumentParser()
parser.add_argument('--openid-configuration',
                    default="https://accounts.google.com/.well-known/openid-configuration")
parser.add_argument('--client-id')
parser.add_argument('--client-secret')
parser.add_argument('--pomerium-client-id')
parser.add_argument('--code')
parser.add_argument('--pomerium-token-url',
                    default="https://authenticate.corp.beyondperimeter.com/api/v1/token")
parser.add_argument('--pomerium-token')
parser.add_argument('--pomerium-url', default="https://httpbin.corp.beyondperimeter.com/get")


def main():
    args = parser.parse_args()
    code = args.code
    pomerium_token = args.pomerium_token
    oidc_document = requests.get(args.openid_configuration).json()
    token_url = oidc_document['token_endpoint']
    print(token_url)
    sign_in_url = oidc_document['authorization_endpoint']

    if not code and not pomerium_token:
        if not args.client_id:
            print("client-id is required")
            sys.exit(1)

        sign_in_url = "{}?response_type=code&scope=openid%20email&access_type=offline&redirect_uri=urn:ietf:wg:oauth:2.0:oob&client_id={}".format(
            oidc_document['authorization_endpoint'], args.client_id)
        print("Access code not set, so we'll do the process interactively!")
        print("Go to the url : {}".format(sign_in_url))
        code = input("Complete the login and enter your code:")
        print(code)

    if not pomerium_token:
        req = requests.post(
            token_url, {
                'client_id': args.client_id,
                'client_secret': args.client_secret,
                'code': code,
                'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob',
                'grant_type': 'authorization_code'
            })

        refresh_token = req.json()['refresh_token']
        print("refresh token: {}".format(refresh_token))

        print("create a new id_token with our pomerium app as the audience")
        req = requests.post(
            token_url, {
                'refresh_token': refresh_token,
                'client_id': args.client_id,
                'client_secret': args.client_secret,
                'audience': args.pomerium_client_id,
                'grant_type': 'refresh_token'
            })
        id_token = req.json()['id_token']
        print("pomerium id_token: {}".format(id_token))

        print("exchange our identity providers id token for a pomerium bearer token")
        req = requests.post(args.pomerium_token_url, {'id_token': id_token})
        pomerium_token = req.json()['Token']
        print("pomerium bearer token is: {}".format(pomerium_token))

    req = requests.get(args.pomerium_url, headers={'Authorization': 'Bearer ' + pomerium_token})
    json_formatted = json.dumps(req.json(), indent=1)
    print(json_formatted)


if __name__ == '__main__':
    main()

Bash

#!/bin/bash
# Create a new OAUTH2 provider DISTINCT from your pomerium configuration
# Select type as "OTHER"
CLIENT_ID='REPLACE-ME.apps.googleusercontent.com'
CLIENT_SECRET='REPLACE-ME'
SIGNIN_URL='https://accounts.google.com/o/oauth2/v2/auth?client_id='$CLIENT_ID'&response_type=code&scope=openid%20email&access_type=offline&redirect_uri=urn:ietf:wg:oauth:2.0:oob'

# This would be your pomerium client id
POMERIUM_CLIENT_ID='REPLACE-ME.apps.googleusercontent.com'

echo "Follow the following URL to get an offline auth code from your IdP"
echo $SIGNIN_URL

read -p 'Enter the authorization code as a result of logging in: ' CODE
echo $CODE

echo "Exchange our authorization code to get a refresh_token"
echo "refresh_tokens can be used to generate indefinite access tokens / id_tokens"
curl \
	-d client_id=$CLIENT_ID \
	-d client_secret=$CLIENT_SECRET \
	-d code=$CODE \
	-d redirect_uri=urn:ietf:wg:oauth:2.0:oob \
	-d grant_type=authorization_code \
	https://www.googleapis.com/oauth2/v4/token

read -p 'Enter the refresh token result:' REFRESH_TOKEN
echo $REFRESH_TOKEN

echo "Use our refresh_token to create a new id_token with an audience of pomerium's oauth client"
curl \
	-d client_id=$CLIENT_ID \
	-d client_secret=$CLIENT_SECRET \
	-d refresh_token=$REFRESH_TOKEN \
	-d grant_type=refresh_token \
	-d audience=$POMERIUM_CLIENT_ID \
	https://www.googleapis.com/oauth2/v4/token

echo "now we have an id_token with an audience that matches that of our pomerium app"
read -p 'Enter the resulting id_token:' ID_TOKEN
echo $ID_TOKEN

curl -X POST \
	-d id_token=$ID_TOKEN \
	https://authenticate.corp.beyondperimeter.com/api/v1/token

read -p 'Enter the resulting Token:' POMERIUM_ACCESS_TOKEN
echo $POMERIUM_ACCESS_TOKEN

echo "we have our bearer token that can be used with pomerium now"
curl \
	-H "Authorization: Bearer ${POMERIUM_ACCESS_TOKEN}" \
	"https://httpbin.corp.beyondperimeter.com/"