Programmatic access
This page describes how to obtain Pomerium access credentials programmatically via a web-based oauth2 style authorization flow. If you have ever used Google's gcloud
commandline app, the mechanism is very similar.
Components
Login API
The API returns a cryptographically signed sign-in URL that can be used to complete a user-driven login process with Pomerium and your identity provider. The login API endpoint takes a pomerium_redirect_uri
query parameter as an argument, which points to the location of the callback server to be called following a successful login.
Here's a full example:
# we'll call the hidden pomerium path below against a proxied-by-pomerium
# service, like our verify app below
ANY_POMERIUM_PROXIED_SERVICE=verify.example.com
# the service we're developing locally, this needs to be localhost to work with
# `pomerium_redirect_uri`, see **NOTE** below, to override this default
MY_LOCAL_DEV_SERVICE=http://localhost:8000
# create a request to the pomerium-proxied service
# `/.pomerium/...` is available for any proxied service
curl "https://$ANY_POMERIUM_PROXIED_SERVICE/.pomerium/api/v1/login?pomerium_redirect_uri=$MY_LOCAL_DEV_SERVICE"
# will output a URL like:
# https://authenticate.example.com/.pomerium/sign_in?pomerium_redirect_uri=http%3A%2F%2Flocalhost%3Fpomerium_callback_uri%3Dhttps%253A%252F%verify.example.com%252F.pomerium%252Fapi%252Fv1%252Flogin%253Fpomerium_redirect_uri%253Dhttp%253A%252F%252Flocalhost&sig=hsLuzJctmgsN4kbMeQL16fe_FahjDBEcX0_kPYfg8bs%3D&ts=1573262981
# open url above in a browser and you'll get redirected in the browser to
# > http://$MY_LOCAL_DEV_SERVICE/?pomerium_jwt=a.real.jwt or expanded as
# http://localhost:8000/?pomerium_jwt=programmatic.pomerium.jwt
# you can now use the value from `pomerium_jwt` to authorize to our proxied endpoint (which you could use to proxy `localhost`)
curl -H 'Authorization: Pomerium a.real.jwt' https://verify.example.com
The value of pomerium_jwt
is an opaque token, meaning the token does not carry identifying information about the user. Unlike encrypted JWTs used for user verification, Pomerium's opaque token functions as an identifier to authenticate against the API, so you should not inspect or rely on the token's values.
To learn more about JWTs and identity verification, see the following docs:
service.example.com
is our endpoint fronted by pomerium-proxylocalhost:8000
is our service we're developing locally, it'll need to accept the programmatic token directly as a query param?pomerium_jwt=programmatic.pomerium.jwt
(see callback handler)authenticate.example.com
is the pomerium-authenticate service, we'll open that in the browser to authenticate
Note: By default only localhost
URLs are allowed as the pomerium_redirect_uri
. This can be customized with the programmatic_redirect_domain_whitelist
option.
Alternative to Login API for localhost
development
Alternatively you can create a new policy to route an endpoint to a bastion host. You should include a HTTP proxy on this bastion host for HTTPS traffic. Here's one way to do it with nginx: https://jerrington.me/posts/2019-01-29-self-hosted-ngrok.html An HTTP proxy on the bastion allows us to receive HTTPS traffic with a self signed cert through LetsEncrypt.
This alternative will allow you to act as if your service is deployed and fronted by Pomerium. We will then forward the remote port from the bastion host behind the pomerium-proxy to localhost.
This is useful if you're using pass_identity_headers
in your policy.
For example:
# a policy like
- from: https://my-dev-endpoint.example.com
to: https://my-bastion-host.example.com:5000
pass_identity_headers: true
Once this policy is applied and deployed, you can then forward the remote port of the HTTP proxy running on the bastion host that in this case proxies 5000 to 5001 internally.
We then forward the remote port from the bastion's HTTP proxy (5001) to localhost:8000
, with an ssh tunnel like:
ssh -N -R 5001:localhost:8000 my-user@my-bastion-host.example.com
You can then go to https://my-dev-endpoint.example.com
and have the pomerium-proxy route traffic securely to the bastion host and back through the ssh-tunnel, the headers and anything pomerium-proxy is setup to do to the request will be included in the forwarded request and traffic.
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 pomerium_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
query parameters.
See the python script below for example of how to start a callback server, and store the session payload.
Handling expiration and revocation
Your script or application should anticipate the possibility that your underlying 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 verify.corp.domain.example
:
- The script or application requests a new login url from the pomerium managed endpoint (e.g.
https://verify.corp.domain.example/.pomerium/api/v1/login
) and takes apomerium_redirect_uri
as an argument. - The script or application opens a browser or redirects the user to the returned login page.
- The user completes the identity providers login flow.
- The identity provider makes a callback to pomerium's authenticate service (e.g.
authenticate.corp.domain.example
) . - Pomerium's authenticate service creates a user session and redirect token, then redirects back to the managed endpoint (e.g.
verify.corp.domain.example
) - Pomerium's Proxy service makes a callback request to the original
pomerium_redirect_uri
with the user session as an argument. - The script or application is responsible for handling that http callback request, and securely handling the callback session (
pomerium_jwt
) queryparam. - The script or application can now make any requests as normal to the upstream application by setting the
Authorization: Pomerium ${pomerium_jwt}
header.
Pomerium supports :
Authorization: Bearer Pomerium-${pomerium_jwt}
X-Pomerium-Authorization: ${pomerium_jwt}
in addition to the Authorization: Pomerium ${pomerium_jwt}
header format.
Example Code
Please see the following minimal but complete python example.
python3 scripts/programmatic_access.py \
--dst https://verify.example.com/headers
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://verify.example.com/json",
)
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):
self.jwt = jwt
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],
)
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 __name__ == "__main__":
main()