Dev guideAPI Reference
Dev guideAPI ReferenceUser GuideGitHubNuGetDev CommunitySubmit a ticketLog In
GitHubNuGetDev CommunitySubmit a ticket

Authorization for Agent

Optimizely Agent supports authorization workflows based on OAuth and JWT standards, allowing you to protect access to its API and Admin interfaces.

There are three modes of operation:

  1. Issuer & Validator
    • Access tokens are issued by Optimizely Agent itself, using a Client Credentials grant. Access tokens are signed and validated using the HS256 algorithm with a signing secret provided in the configuration. Clients request access tokens by sending a POST request to /oauth/token on the port of the desired interface (by default, 8080 for the API interface, and 8088 for the Admin interface), including a client ID and secret in the request.
    • Issuer & Validator mode is useful if you want to implement authorization and you are not already running an authorization server that can issue JWTs.
  2. Validator-only
    • Agent validates access tokens that were issued elsewhere. Access tokens are validated with public keys fetched from a JWKS URL provided in the configuration.
    • Validator-only mode is useful if you want to plug directly into an existing JWT-based workflow already being used in your system or organization.
  3. No authorization (default)
    • The interface is publicly available.

Configuration

  • The API and Admin interfaces are each independently configured to run in one of the above-mentioned modes of operation.
  • Authorization configuration is located under the auth key
  • Each mode of operation has its own set of configuration properties, described below.

1. Issuer & Validator

The configuration properties pertaining to Issuer & Validator mode are listed below:

Property NameEnvironment VariableDescription
ttlTTLTime-to-live of access tokens issued
hmacSecretsHMACSECRETSArray of secrets used to sign & validate access tokens, using the HMAC SHA256 algorithm. Values must be base64-format strings. The first value in the array is used to sign issued access tokens. Access tokens signed with any value in the array are considered valid
clientsN/AArray of objects, used for token issuance, consisting of id, secretHash, and sdkKeys. Clients provide ID and secret in their requests to /oauth/token. Agent validates the request credentials by checking for an exact match of ID, checking that the BCrypt hash of the request secret matches the secretHash from configuration, and that the SDK key provided in the X-Optimizely-Sdk-Key request header exists in the sdkKeys from configuration. secretHash values must be base64-format strings.

To make setup easier, Agent provides a command-line tool that can generate base64-encoded 32-byte random values, and their associated base64-encoded BCrypt hashes:

// From the Agent root directory
> make generate_secret
Client Secret: i3SrdrCy/wEGqggv9OI4FgIsdHHNpOacrmIMJ6SFIkE=
Client Secret's hash: JDJhJDEyJERGNzhjRXVTNTdOQUZ3cndxTkZ6Li5XQURlazU2R21YeFZjb1pWSkN5eGZ1SXM4VXRLb0ZD

Use the hash value to configure Agent, and pass the secret value as client_secret when making access token requests to /oauth/token. For details of the access token issuance endpoint, see the OpenAPI spec file..

2. Validator-only

The configuration properties pertaining to Validator-only mode are listed below:

Property NameEnvironment VariableDescription
jwksURLJWKSURLURL from which public keys should be fetched for token validation
jwksUpdateIntervalJWKSUPDATEINTERVALInterval on which public keys should be re-fetched (example: 30m for 30 minutes)

3. No authorization (default)

The API & Admin interfaces run with no authorization when no auth configuration is given.

Configuration examples

Optimizely Agent uses the Viper library for configuration, which allows setting values via environment variables, flags, and YAML configuration files.

1. Issuer & Validator

️Warning

For security, we advise that you configure hmacSecrets with either an environment variable or a flag, and NOT through a config file.

In the below example, the Admin interface is configured in Issuer & Validator mode, with hmacSecrets provided via environment variable, and other values provided via YAML config file.

// Comma-separated value, to set multiple hmacSecrets.
// Access tokens are signed with the first value.
// Access tokens are valid when they are signed with either of these values.
export OPTIMIZELY_ADMIN_HMACSECRETS=QPtUGP/RqaXRltZf1QE1KxlF2Iuo09J0buZ3UNKeIr0,bkZAqSsZuM5NSnwEyO9Pzb6F8gGNu1BBuX/SpPaMeyM
admin:
    auth:
        # Access tokens will expire after 30 minutes
        ttl: 30m
        clients:
            # Either of these two id/secret pairs can be exchanged for access tokens
            - id: agentConsumer1
            secretHash: XgZTeTvWaZ6fLiey6EBSOxJ2QFdd6dIiUcZGDIIJ+IY 
            sdkKeys:
              # These credentials can be exchanged for tokens granting access to these two SDK keys
              - abcd1234
              - efgh5678
            - id: agentConsumer2
            secretHash: ssz0EEViKIinkFXxzqncKxz+6VygEc2d2rKf+la5rXM 
            sdkKeys:
              # These credentials can be exchanged for tokens granting access only to this one SDK key
              - ijkl9012

2. Validator-only

# In this example, the API interface is configured in Validator-only mode
api:
    auth:
        # Signing keys will be fetched from this url and used when validating access tokens
        jwksURL: https://YOUR_DOMAIN/.well-known/jwks.json
        # Siging keys will be periodically fetched on this interval
        jwksUpdateInterval: 30m

Secret rotation (issuer & validator mode)

To support secret rotation, both hmacSecrets and clients support setting multiple values. In hmacSecrets, the first value will be used to sign issued tokens, but tokens signed with any of the values will be considered valid.

Example - Python

#!/usr/bin/python

import json
import requests
import sys

# This example demonstrates interacting with Agent running in Issuer & Validator mode.
# We obtain an access token and use it to request the current Optimizely Config
# from the API interface.

# Fist, we need a secret value to sign access tokens.
# You can use the generate_secret tool included with Agent to generate this:

# > make generate_secret
# Client Secret: CvzvkWm3V1D9RBxPWEjC+ud9zvwcOvnnLkWaIkzDGyA=

# You can ignore the second line that says "Client Secret's hash".

# Then, set an environment variable to make this secret available to Agent:
# > export OPTIMIZELY_API_AUTH_HMACSECRETS=CvzvkWm3V1D9RBxPWEjC+ud9zvwcOvnnLkWaIkzDGyA=

# Next, we need client credentials (ID & secret), and the BCrypt hash of our secret
# Again, you can use the generate_secret tool included with Agent to generate these:
#
# > make generate_secret
# Client Secret: 0bfLVX9U3Lpr6Qe4X3DSSIWNqEkEQ4bkX1WZ5Km6spM=
# Client Secret's hash: JDJhJDEyJEdkSHpicHpRODBqOC9FQzRneGIyNXU0ZFVPMFNKcUhkdTRUQXRzWUJOdjRzRmcuVGdFUTUu
#
# Take the hash, and add it to your agent configuration file (default: config.yaml) under the "api" section,
# along with your desired client ID and SDK key:
#
# auth:
#   ttl: 30m
#   clients:
#     - id: clientid1
#       secretHash: JDJhJDEyJEdkSHpicHpRODBqOC9FQzRneGIyNXU0ZFVPMFNKcUhkdTRUQXRzWUJOdjRzRmcuVGdFUTUu
#       sdkKeys:
#           - <Your SDK Key>

#
# Start Agent with the API interface running on the default port (8080).
# Then, finally, run the example, passing your SDK key, client ID and secret:
# > python auth.py <Your SDK Key> clientid1 0bfLVX9U3Lpr6Qe4X3DSSIWNqEkEQ4bkX1WZ5Km6spM=
#
# For more information, see docs/auth.md

if len(sys.argv) < 4:
    sys.exit('Requires three arguments: <SDK-Key> <Client ID> <Client Secret>')

sdk_key = sys.argv[1]
client_id = sys.argv[2]
client_secret = sys.argv[3]

s = requests.Session()
s.headers.update({'X-Optimizely-SDK-Key': sdk_key})

resp = s.get('http://localhost:8080/v1/config')
print('first config request, not including access token: response status = {}'.format(resp.status_code))

resp = s.post('http://localhost:8080/oauth/token', data={
    'grant_type': 'client_credentials',
    'client_id':  client_id,
    'client_secret': client_secret,
})
resp_dict = resp.json()
print('access token response: ')
print(json.dumps(resp_dict, indent=4, sort_keys=True))

s.headers.update({'Authorization': 'Bearer {}'.format(resp_dict['access_token'])})

resp = s.get('http://localhost:8080/v1/config')
print('config response after passing access token: ')
print(json.dumps(resp.json(), indent=4, sort_keys=True))