Table of Contents

Welcome to the Plan B OAuth2/JWT Infrastructure Documentation!

Introduction

Tip

You can also get a good introduction to Plan B by checking out our meetup talk slides “Plan B Service to Service Authentication with OAuth”.

Plan B provides an OAuth 2 infrastructure with these main features:

  • Service to Service authentication using the Resource Owner Password Credentials Grant and JWT tokens
  • User to Service authentication supporting the Authorization Code Grant flow
  • Token creation without any write operations (no token storage to avoid bottlenecks)
  • Very fast, stateless OAuth access token validation (verifying JWT signature)
  • Highly available Cassandra backend to store OAuth user and client credentials
  • Revocation of JWT access tokens by claims (e.g. “revoke all tokens for user XY”)
  • Support for seamless rotation of OAuth user and client credentials

Plan B was developed with the following non-functional requirements in mind:

  • Robustness & resilience – all Plan B components must be highly available and resilient to error conditions
  • Low latency for token validation – the Token Info component is completely stateless and can be deployed “near” applications to avoid network latency
  • Horizontal scalability – all components (including Cassandra storage) should be able to scale out

Plan B provides three infrastructure components to do Service To Service Authentication with JWT OAuth Bearer tokens:

Provider
OAuth2 Authorization Server and OpenID Connect Provider issuing JWT tokens.
Token Info
OAuth2 tokeninfo validation endpoint for JWT tokens.
Revocation Service
REST service to manage token revocation lists.
Plan B Architecture

Provider

The Plan B Provider issues signed JSON Web Tokens (JWT) as response to Access Token Requests with a valid Resource Owner Password Credentials Grant.

An example token creation response (/oauth2/access_token endpoint):

{
    "access_token": "eyJraWQiOiJ0ZXN0a2V5LWVzMjU2IiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJ0ZXN0MiIsInNjb3BlIjpbImNuIl0sImlzcyI6IkIiLCJyZWFsbSI6Ii9zZXJ2aWNlcyIsImV4cCI6MTQ1NzMxOTgxNCwiaWF0IjoxNDU3MjkxMDE0fQ.KmDsVB09RAOYwT0Y6E9tdQpg0rAPd8SExYhcZ9tXEO6y9AWX4wBylnmNHVoetWu7MwoexWkaKdpKk09IodMVug",
    "id_token": "eyJraWQiOiJ0ZXN0a2V5LWVzMjU2IiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJ0ZXN0MiIsInNjb3BlIjpbImNuIl0sImlzcyI6IkIiLCJyZWFsbSI6Ii9zZXJ2aWNlcyIsImV4cCI6MTQ1NzMxOTgxNCwiaWF0IjoxNDU3MjkxMDE0fQ.KmDsVB09RAOYwT0Y6E9tdQpg0rAPd8SExYhcZ9tXEO6y9AWX4wBylnmNHVoetWu7MwoexWkaKdpKk09IodMVug",
    "token_type": "Bearer",
    "expires_in": 28800,
    "scope": "cn",
    "realm": "/services"
}

The JWT header contains the ID of the signing key and the used algorithm:

{
    "kid": "testkey-es256",
    "alg": "ES256"
}

The JWT payload contains the user ID (sub claim), realm and granted scopes:

{
    "sub": "test2",
    "scope": [
        "cn"
    ],
    "iss": "B",
    "realm": "/services",
    "exp": 1457319814,
    "iat": 1457291014
}

Token Info

The Plan B Token Info validates signed JWT tokens using the right public key (exposed by the Provider) and checks the token against any revocation lists.

The /oauth2/tokeninfo validation response contains details about the OAuth2 access token:

{
    "access_token": "eyJraWQiOiJ0ZXN0a2V5LWVzMjU2IiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJ0ZXN0MiIsInNjb3BlIjpbImNuIl0sImlzcyI6IkIiLCJyZWFsbSI6Ii9zZXJ2aWNlcyIsImV4cCI6MTQ1NzMxOTgxNCwiaWF0IjoxNDU3MjkxMDE0fQ.KmDsVB09RAOYwT0Y6E9tdQpg0rAPd8SExYhcZ9tXEO6y9AWX4wBylnmNHVoetWu7MwoexWkaKdpKk09IodMVug",
    "cn": true,
    "expires_in": 28292,
    "grant_type": "password",
    "open_id": "eyJraWQiOiJ0ZXN0a2V5LWVzMjU2IiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJ0ZXN0MiIsInNjb3BlIjpbImNuIl0sImlzcyI6IkIiLCJyZWFsbSI6Ii9zZXJ2aWNlcyIsImV4cCI6MTQ1NzMxOTgxNCwiaWF0IjoxNDU3MjkxMDE0fQ.KmDsVB09RAOYwT0Y6E9tdQpg0rAPd8SExYhcZ9tXEO6y9AWX4wBylnmNHVoetWu7MwoexWkaKdpKk09IodMVug",
    "realm": "/services",
    "scope": ["cn"],
    "token_type": "Bearer",
    "uid": "test2"
}

Revocation Service

The Plan B Revocation Service manages token revocation lists and provides them to Token Info.

See the Revocations section for details.

OAuth 2.0

Plan B tries to be compliant with RFC 6749 “The OAuth 2.0 Authorization Framework”; the Plan B Provider implements an OAuth Authorization Server with the following features:

Authorization Grant

The Plan B Provider currently supports the following grant types:

Authorization Code Grant Type
Client redirects to Plan B and user logs in with his credentials. This grant type is used for User to Service authentication.
Implicit Grant Type
In the implicit flow, instead of issuing the client an authorization code, the client is issued an access token directly via browser redirect. Plan B allows the implicit flow only for clients which are flagged as “non-confidential”.
Resource Owner Password Credentials Grant Type
User and client credentials are directly exchanged for an JWT access token. This grant type is mostly used for Service to Service authentication where user and client OAuth credentials are known to the service by some form of “secret distribution”.

Access Token

The Plan B Provider issues access tokens in the JWT format which can be used as Bearer Tokens and validated against the Plan B Token Info.

The issued JWT tokens have the following properties:

  • The access token is valid for 8 hours.
  • The JWT is signed with a JSON Web Signature (JWS) and therefore has a corresponding JOSE Header and JWS signature
  • The JOSE header always contains the signing key ID (kid) and signing algorithm (alg)
  • The JWT payload always contains the following claims:
    • subject (sub)
    • realm (realm)
    • scopes (scope)
    • issuer (iss)
    • expiration date (exp)
    • issue date (iat)
  • The JWT payload will additionally contain the OAuth client ID in the azp claim if the “azp” scope was requested (and granted)

An example JWT access token issued by Plan B Provider might look like (line breaks were added for readability):

eyJraWQiOiJ0ZXN0a2V5LWVzMjU2IiwiYWxnIjoiRVMyNTYifQ.
eyJzdWIiOiJ0ZXN0MiIsInNjb3BlIjpbImNuIl0sImlzcyI6IkIiLCJyZWFsbSI6Ii9zZXJ2aWNlcyIsImV4cCI6MTQ1NzMxOTgxNCwiaWF0IjoxNDU3MjkxMDE0fQ.
KmDsVB09RAOYwT0Y6E9tdQpg0rAPd8SExYhcZ9tXEO6y9AWX4wBylnmNHVoetWu7MwoexWkaKdpKk09IodMVug

The JOSE header contains:

{
    "kid": "testkey-es256",
    "alg": "ES256"
}

The JWT payload contains:

{
    "sub": "test2",
    "scope": [
        "cn"
    ],
    "iss": "B",
    "realm": "/services",
    "exp": 1457319814,
    "iat": 1457291014
}

Refresh Token

The Plan B Provider does not support refresh tokens.

Protocol Endpoints

Authorization Endpoint

Plan B Provider provides the OAuth Authorization Endpoint as /oauth2/authorize:

  • The client redirects the user to /oauth2/authorize?response_type=code&client_id={clientId}&realm={realm}
    • response_type must be either “code” (for authorization code flow) or “token” (for implicit flow)
    • client_id must be set
    • realm must either be set or match hostname
    • redirect_uri is the optional callback URL
    • state is the optional client “state” (passed through)
  • Plan B will display a login form
  • Successful user authentication will trigger a redirect back to the client (redirect_uri) including a code query parameter or an access_token query parameter (for implicit flow).
  • For authorization code flow: the client can exchange the given authorization code for a valid JWT token at the Token Endpoint.

See RFC 6749 section 4.1.1. “Authorization Request” for details.

Token Endpoint

The Plan B Provider provides the OAuth Token Endpoint as /oauth2/access_token:

  • The client MUST use the HTTP “POST” method against /oauth2/access_token
  • Confidential clients MUST authenticate with their client ID and secret via HTTP Basic Auth (Authorization header).
  • The realm MUST be passed as either form or query parameter (e.g. /oauth2/access_token?realm=/services)
  • The grant_type parameter MUST have the value “password”.

See RFC 6749 section 4.3.2. “Access Token Request” for details.

Introspection Endpoint

The Plan B Token Info does not yet implement the OAuth 2.0 Token Introspection Endpoint, but instead the endpoint /oauth2/tokeninfo is provided:

  • The access token SHOULD be passed as a Bearer token in the Authorization header.
  • The access token MAY be passed in the access_token query parameter.
  • The response will only have HTTP status code 200 if:
    • the JWS signature is valid
    • the JWT is not expired (i.e. the exp value lies in the future)
    • the token was not revoked
  • The JSON response will at least contain the following properties:
    • seconds till expiry (expires_in)
    • list of granted scopes (scope)
    • user ID (uid)
    • user realm (realm)

OpenID Connect

The Plan B Provider does not yet fully implement all OpenID Provider endpoints.

Authorization Endpoint

The Plan B Provider Authorization Endpoint does not yet support the optional OpenID Connect request parameters.

Token Endpoint

The Plan B Provider Token Endpoint returns an ID token (id_token).

OpenID Provider Configuration Information Endpoint

The Plan B Provider supports the OpenID Connect Discovery endpoint /.well-known/openid-configuration.

Only the jwks_uri has a meaningful value in the OpenID Provider Configuration Response.

Getting Started

All three Plan B components can be easily built and started locally. You will need:

  • Java 8 JDK for Provider and Revocation Service
  • Go 1.6 for Token Info
  • Cassandra 2.1 for Provider and Revocation Service (can be started as Docker container)

Follow the README instructions in the respective repositories to build the components:

Start Cassandra and create the necessary keyspaces for Provider and Revocation (we assume source was checked out into planb-provider and planb-revocation):

$ docker run --name cassandra -d -p 9042:9042 cassandra:2.1
$ docker run -i --link cassandra:cassandra --rm cassandra:2.1 cqlsh cassandra < planb-provider/schema.cql
$ docker run -i --link cassandra:cassandra --rm cassandra:2.1 cqlsh cassandra < planb-revocation/schema.cql

You can use the generate-load-test-data.py script to generate a test key pair and test credentials:

$ sudo pip3 install bcrypt # install required Python module to hash passwords
$ ./planb-provider/scripts/generate-load-test-data.py > test-data.cql
$ docker run -i --link cassandra:cassandra --rm cassandra:2.1 cqlsh cassandra < test-data.cql

This will create a bunch of test service users and clients, e.g.:

  • username “test0” and password “test0”
  • client ID “test0” and client secret “test0”

Now start the Provider (default port 8080) and Token Info (default port 9021).

$ export OAUTH2_ACCESS_TOKENS=customerLogin=test             # fixed OAuth test token (unused)
$ export TOKENINFO_URL=https://example.com/oauth2/tokeninfo  # required for /raw-sync REST API (unused here)
$ java -jar planb-provider/target/planb-provider-1.0-SNAPSHOT.jar &

$ export OPENID_PROVIDER_CONFIGURATION_URL=http://localhost:8080/.well-known/openid-configuration
$ export UPSTREAM_TOKENINFO_URL=https://auth.example.org/oauth2/tokeninfo
$ export REVOCATION_PROVIDER_URL=https://planb-revocation.example.org/revocations
$ $GOPATH/bin/planb-tokeninfo

Let’s first check that our OpenID Provider runs and contains at least one test key pair:

$ curl http://localhost:8080/.well-known/openid-configuration # should contain jwks_uri
$ curl http://localhost:8080/oauth2/connect/keys # should return at least one public key

We can create a new JWT token with cURL:

$ curl -u test0:test0 \
  -d 'grant_type=password&username=test0&password=test0' \
  http://localhost:8080/oauth2/access_token?realm=/services

Afterwards we can validate the JWT using the Token Info:

$ TOKEN=... # insert "access_token" value from above
$ curl -H "Authorization: Bearer $TOKEN" http://localhost:9021/oauth2/tokeninfo

Realms

Plan B supports the “realms” concept to configure different properties and backends for different use cases. Usernames (sub claim or uid Token Info response property) are only unique within one realm, i.e. Plan B allows to have two different users with the same username “jdoe” in two different realms (e.g. “jdoe” customer and “jdoe” employee).

By default, the following realms are defined:

/services
Service users (applications) which are authenticated using the Cassandra storage. See the section on Service To Service Authentication.
/employees
Human users which are authenticated against an upstream (OAuth) service.
/customers
Special realm for “customers” which are authenticated against a specific, proprietary Customer Service web service.

Client credentials are always checked against Plan B’s Cassandra storage, but user authentication might be delegated to upstream services (done by default for realms “/employees” and “/customers”).

Different realms can be configured via the Provider’s Spring configuration files or environment variables:

$ cd planb-provider
$ ./mvnw verify
$ export REALM_NAMES=/myrealm,/otherrealm
$ java -jar target/planb-provider-1.0-SNAPSHOT.jar

The realm is always included in the JWT payload as the realm claim. Plan B’s Token Info will return the token’s realm in the “realm” property; this can be used for authorization rules in resource servers, e.g.:

  • allow all tokens with the “/employees” realm to read data
  • disallow any access for tokens with the “/customers” realm

Service To Service Authentication

The main driver for Plan B’s development was enabling “Service to Service” authentication with OAuth Bearer tokens.

Application A (client) needs to authenticate itself, so that application B (resource server) can grant access:

Plan B Architecture
  • Service user and OAuth client for application A are created in the Plan B Provider
  • A’s user and client credentials are distributed (e.g. via S3 buckets), so that A can read them
  • Application A gets a new OAuth access token (JWT) from the Plan B Provider
  • The client A calls the desired HTTP endpoint of application B, passing the JWT token in the Authorization header (“Bearer {token}”)
  • The resource server B validates the token by calling the Token Info endpoint
  • Token Info will verify the JWT signature and returns a JSON token info structure with A’s service user ID in the uid property

For this to work, both applications need to know about the authentication infrastructure:

Credential Rotation

It’s good practice to rotate all credentials regularly:

  • OAuth service user passwords should be rotated frequently, e.g. every two hours.
  • OAuth client credentials (client ID and secret for confidential clients) might be rotated every month.

The Provider allows rotating both user and client credentials via its /raw-sync/ REST API.

Service Users

To rotate an application’s user credentials, do:

  • Add a new password by sending a POST request to /raw-sync/users/{realm}/{id}/password.
  • Distribute the new password (e.g. via a S3 Mint Bucket).
  • Wait at least 10 minutes to give the application time to pick up the new password. Both old and new password will be active during this grace period.
  • Commit the new password by overwriting the user’s passwords via a PATCH request to /raw-sync/users/{realm}/{id}.

Clients

OAuth client credentials can be rotated by creating a new client and deactivating or deleting the old one:

  • Create a new client by sending a PUT request to /raw-sync/clients/{realm}/{id} (see below).
  • Distribute the new client credentials (e.g. via a S3 Mint Bucket).
  • Wait at least 10 minutes to give the application time to pick up the new client credentials. Both old and new client will be active during this grace period.
  • Reset the old client’s secret to some random string or delete the old client completely.

Example REST call to create a new client in the /services realm using HTTPie:

$ SECRET_HASH=$(python3 -c "import bcrypt; print(bcrypt.hashpw(b'test', bcrypt.gensalt(4)).decode('ascii'))")
$ TOK='mytoken' # valid OAuth token
$ http PUT https://planb-provider.example.org/raw-sync/clients/services/myclient-123 \
  is_confidential:=true \
  name='Example Client'\
  secret_hash=$SECRET_HASH \
  "Authorization:Bearer $TOK"

Revocations

Plan B allows revoking JWT tokens via three different revocation types:

TOKEN
Revoke single JWT tokens.
CLAIM
Revoke all JWTs having a specific claim value.
GLOBAL
Revoke all JWTs issued before a certain date.

Revocations are stored in Cassandra and the Token Info component regularly polls for deltas.

Revoking a Single Token

$ tok=... # some valid token accepted by the configured TOKENINFO_URL
$ curl -X POST \
     -H "Authorization: Bearer $tok" \
     -H 'Content-Type: application/json' \
     -d '{"type": "TOKEN", "data": {"token": "..."}}' \
     "https://planb-revocation.example.org/revocations"

Revoking Tokens by Claims

Revoking all tokens issued up to now with subject (username) “jdoe”:

$ tok=... # some valid token accepted by the configured TOKENINFO_URL
$ curl -X POST \
     -H "Authorization: Bearer $tok" \
     -H 'Content-Type: application/json' \
     -d '{"type": "CLAIM", "data": {"claims": {"sub": "jdoe"}}}' \
     "https://planb-revocation.example.org/revocations"

Forcing Token Info to refresh from certain Timestamp

$ tok=... # some valid token accepted by the configured TOKENINFO_URL
$ curl -X POST \
    -H "Authorization: Bearer $tok" \
    https://planb-revocation.example.org/notifications/REFRESH_FROM?value=123

Monitoring

Cassandra

The Cassandra cluster should be closely monitored using the usual Cassandra JMX beans, e.g.:

  • Number of UP/DOWN nodes
  • Number of Read/Write requests per second
  • Read/Write latency

Provider

The Provider exposes metrics on its management port 7979 with path /metrics.

planb.provider.access_token.{realm}.success
DropWizard timer for successful access token requests by realm.
planb.provider.access_token.{realm}.error.{errortype}
DropWizard timer for failed access token requests by realm and error type.

The Provider is mostly CPU-bound by its BCrypt password checking and JWT signing operations. Monitoring the CPU usage percentage is therefore strongly recommended.

Token Info

The Token Info exposes metrics on its metrics port 9020 with path /metrics.

planb.openidprovider.numkeys
Number of public keys in memory.
planb.tokeninfo.jwt.{realm}.requests
Timer for the JWT handler (one timer per realm).
planb.tokeninfo.proxy
Timer for the proxy handler (includes cached results and upstream calls).
planb.tokeninfo.proxy.cache.hits
Number of upstream cache hits.
planb.tokeninfo.proxy.cache.misses
Number of upstream cache misses.
planb.tokeninfo.proxy.cache.expirations
Number of upstream cache misses because of expiration.
planb.tokeninfo.proxy.upstream
Timer for calls to the upstream tokeninfo. Cached responses are not measured here.

The Token Info is mostly CPU-bound by its JWT signature verification operations. Monitoring the CPU usage percentage is therefore strongly recommended.

Integrations

STUPS

Plan B can be seamlessly integrated into the STUPS infrastructure:

  • The mint worker can automatically create and rotate application OAuth users for all applications registered in Kio
  • The mint worker can distribute OAuth credentials via S3 buckets

Libraries

The following libraries are compatible with Plan B:

Clients

There are many libraries for the OAuth2 authorization code grant flow (which is supported by Plan B), e.g. the Flask-OAuth library for Python.

Resource Servers

  • The Python Flask REST framework Connexion supports token validation against Plan B Token Info
  • The Clojure REST framework Friboo supports token validation against Plan B Token Info
  • The Java Spring-OAuth2 STUPS Support library supports token validation against Plan B Token Info when implementing resource servers with the Spring framework