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.
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
)
- subject (
- 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 setrealm
must either be set or match hostnameredirect_uri
is the optional callback URLstate
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 acode
query parameter or anaccess_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
)
- seconds till expiry (
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:
- https://github.com/zalando/planb-provider
- https://github.com/zalando/planb-tokeninfo
- https://github.com/zalando/planb-revocation
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:
- 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:
- A needs to know its service user (username and password) and client credentials (client ID and secret)
- A needs to know the OAuth2 access token URL (
OAUTH2_ACCESS_TOKEN_URL
environment variable) to create JWT tokens, e.g. https://planb-provider.example.org/oauth2/access_token - B needs to know the OAuth2 token info URL (
TOKENINFO_URL
environment variable), e.g. https://planb-tokeninfo.example.org/oauth2/tokeninfo
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¶
- The STUPS Java tokens library supports token creation using the Plan B Provider Token Endpoint
- The STUPS Python tokens library supports token creation using the Plan B Provider Token Endpoint
- The Go tokens library also supports token creation using the Plan B Provider Token Endpoint
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