Prime Services Authentication

Galaxy API Authentication Guide

This guide covers authentication for all Galaxy APIs using OAuth 2.0 Client Credentials flow.

Overview

Galaxy APIs use OAuth 2.0 with the Client Credentials grant type for machine-to-machine (M2M) authentication. You'll need:

  • Client ID - Provided during onboarding
  • Client Secret - Provided during onboarding (keep this secure!)

Getting an Access Token

Token Endpoint

Each environment has a branded authentication endpoint powered by Okta:

EnvironmentAuth DomainAuth Server ID
UATauth.api.uat.galaxy.comausy7o2fb3m86Kc8Z4x7
Productionauth.api.galaxy.comausy7o2fb3m86Kc8Z4x7

Token Endpoint URL Pattern:

https://{auth_domain}/oauth2/{auth_server_id}/v1/token

Example for Production:

https://auth.api.galaxy.com/oauth2/ausy7o2fb3m86Kc8Z4x7/v1/token

Request

# Production example
curl -X POST "https://auth.api.galaxy.com/oauth2/ausy7o2fb3m86Kc8Z4x7/v1/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "scope=api:read"

Request Parameters:

ParameterRequiredDescription
grant_typeYesMust be client_credentials
client_idYesYour client ID
client_secretYesYour client secret
scopeYesSpace-separated list of scopes (see Scopes)

Response

Success (200 OK):

{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "eyJraWQiOiJKV1..."
}
FieldDescription
token_typeAlways Bearer
expires_inToken lifetime in seconds (typically 3600 = 1 hour)
access_tokenJWT to include in API requests

Error (400 Bad Request):

{
  "error": "invalid_client",
  "error_description": "Client authentication failed."
}
Error CodeDescription
invalid_clientInvalid client credentials
invalid_scopeRequested scope is not authorized for this client
invalid_requestMissing required parameter

Using the Access Token

Include the access token in the Authorization header for all API requests:

curl https://api.galaxy.com/service-reference-g1/accounts \
  -H "Authorization: Bearer eyJraWQiOiJKV1..."

Scopes

ScopeDescription
api:readRead access to APIs (required for all requests)
api:writeWrite access to APIs (required for POST/PUT/DELETE)

Request only the scopes you need. Most read-only integrations only need api:read.

Token Refresh Best Practices

Access tokens expire after the time specified in expires_in (typically 1 hour). Follow these best practices:

1. Cache and Reuse Tokens

Don't request a new token for every API call. Cache the token and reuse it until it expires.

import time
import requests

class GalaxyApiClient:
    # Auth server IDs per environment
    AUTH_SERVERS = {
        "dev": "auszxcyntf9RscPsW1d7",
        "qa": "auszxcyntf9RscPsW1d7",
        "uat": "ausy7o2fb3m86Kc8Z4x7",
        "prod": "ausy7o2fb3m86Kc8Z4x7",
    }

    def __init__(self, client_id, client_secret, env="prod"):
        self.client_id = client_id
        self.client_secret = client_secret
        self.env = env

        # Set URLs based on environment
        if env == "prod":
            self.base_url = "https://api.galaxy.com"
            self.auth_domain = "auth.api.galaxy.com"
        else:
            self.base_url = f"https://api.{env}.galaxy.com"
            self.auth_domain = f"auth.api.{env}.galaxy.com"

        auth_server_id = self.AUTH_SERVERS[env]
        self.auth_url = f"https://{self.auth_domain}/oauth2/{auth_server_id}/v1/token"

        self._token = None
        self._token_expires_at = 0

    def _get_token(self):
        """Get a valid access token, refreshing if needed."""
        # Refresh 5 minutes before expiry to avoid race conditions
        if self._token and time.time() < (self._token_expires_at - 300):
            return self._token

        response = requests.post(
            self.auth_url,
            data={
                "grant_type": "client_credentials",
                "client_id": self.client_id,
                "client_secret": self.client_secret,
                "scope": "api:read"
            }
        )
        response.raise_for_status()

        data = response.json()
        self._token = data["access_token"]
        self._token_expires_at = time.time() + data["expires_in"]

        return self._token

    def get(self, path):
        """Make an authenticated GET request."""
        return requests.get(
            f"{self.base_url}{path}",
            headers={"Authorization": f"Bearer {self._get_token()}"}
        )

2. Proactive Token Refresh

Refresh the token before it expires to avoid failed requests:

# Refresh 5 minutes (300 seconds) before expiry
REFRESH_BUFFER_SECONDS = 300

if time.time() > (token_expires_at - REFRESH_BUFFER_SECONDS):
    refresh_token()

3. Handle 401 Responses Gracefully

If you receive a 401 Unauthorized response, refresh the token and retry once:

response = api_client.get("/accounts")

if response.status_code == 401:
    # Token may have been revoked or expired
    api_client.refresh_token()
    response = api_client.get("/accounts")

if response.status_code == 401:
    # Still failing - credentials may be invalid
    raise AuthenticationError("Failed to authenticate")

4. Don't Hardcode Token Expiry

Always use the expires_in value from the token response. Token lifetimes may change.

# ✅ Good - use expires_in from response
token_expires_at = time.time() + response["expires_in"]

# ❌ Bad - hardcoded expiry
token_expires_at = time.time() + 3600

Error Handling

Authentication Errors

HTTP StatusErrorAction
400invalid_clientCheck client_id and client_secret
400invalid_scopeRequest only scopes your client is authorized for
401Token expiredRefresh the token and retry
403ForbiddenYour client doesn't have access to this API

Rate Limiting

The token endpoint has rate limits. If you receive a 429 Too Many Requests:

  1. Wait for the duration specified in the Retry-After header
  2. Review your token caching - you may be requesting tokens too frequently

OpenID Connect Discovery

For programmatic configuration, use the OpenID Connect discovery endpoint:

Discovery URL Pattern:

https://{auth_domain}/oauth2/{auth_server_id}/.well-known/openid-configuration
EnvironmentDiscovery URL
Developmenthttps://auth.api.dev.galaxy.com/oauth2/auszxcyntf9RscPsW1d7/.well-known/openid-configuration
QAhttps://auth.api.qa.galaxy.com/oauth2/auszxcyntf9RscPsW1d7/.well-known/openid-configuration
UAThttps://auth.api.uat.galaxy.com/oauth2/ausy7o2fb3m86Kc8Z4x7/.well-known/openid-configuration
Productionhttps://auth.api.galaxy.com/oauth2/ausy7o2fb3m86Kc8Z4x7/.well-known/openid-configuration

This returns metadata including:

{
  "issuer": "https://auth.api.galaxy.com/oauth2/ausy7o2fb3m86Kc8Z4x7",
  "token_endpoint": "https://auth.api.galaxy.com/oauth2/ausy7o2fb3m86Kc8Z4x7/v1/token",
  "jwks_uri": "https://auth.api.galaxy.com/oauth2/ausy7o2fb3m86Kc8Z4x7/v1/keys",
  "scopes_supported": ["openid", "api:read", "api:write"],
  "grant_types_supported": ["client_credentials"]
}

Security Best Practices

  1. Never expose client secrets in client-side code, logs, or version control
  2. Use environment variables or a secrets manager for credentials
  3. Rotate credentials periodically - contact Galaxy support to rotate your client secret
  4. Use TLS - all Galaxy API endpoints require HTTPS
  5. Validate the issuer when verifying JWTs to prevent token confusion attacks

Code Examples

Python

import requests

# Production auth server ID
AUTH_SERVER_ID = "ausy7o2fb3m86Kc8Z4x7"
TOKEN_URL = f"https://auth.api.galaxy.com/oauth2/{AUTH_SERVER_ID}/v1/token"

def get_access_token(client_id, client_secret):
    response = requests.post(
        TOKEN_URL,
        data={
            "grant_type": "client_credentials",
            "client_id": client_id,
            "client_secret": client_secret,
            "scope": "api:read"
        }
    )
    response.raise_for_status()
    return response.json()["access_token"]

Node.js

const axios = require('axios');

// Production auth server ID
const AUTH_SERVER_ID = 'ausy7o2fb3m86Kc8Z4x7';
const TOKEN_URL = `https://auth.api.galaxy.com/oauth2/${AUTH_SERVER_ID}/v1/token`;

async function getAccessToken(clientId, clientSecret) {
  const response = await axios.post(
    TOKEN_URL,
    new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: clientId,
      client_secret: clientSecret,
      scope: 'api:read'
    }),
    {
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
    }
  );
  return response.data.access_token;
}

Java

import java.net.http.*;
import java.net.URI;

public class GalaxyAuth {
    // Production auth server ID
    private static final String AUTH_SERVER_ID = "ausy7o2fb3m86Kc8Z4x7";
    private static final String TOKEN_URL =
        "https://auth.api.galaxy.com/oauth2/" + AUTH_SERVER_ID + "/v1/token";

    public static String getAccessToken(String clientId, String clientSecret) throws Exception {
        HttpClient client = HttpClient.newHttpClient();

        String body = String.format(
            "grant_type=client_credentials&client_id=%s&client_secret=%s&scope=api:read",
            clientId, clientSecret
        );

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(TOKEN_URL))
            .header("Content-Type", "application/x-www-form-urlencoded")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = client.send(request,
            HttpResponse.BodyHandlers.ofString());

        // Parse JSON response to extract access_token
        // (use your preferred JSON library)
        return parseAccessToken(response.body());
    }
}

cURL

# Production auth server ID
AUTH_SERVER_ID="ausy7o2fb3m86Kc8Z4x7"

# Get token
TOKEN=$(curl -s -X POST "https://auth.api.galaxy.com/oauth2/${AUTH_SERVER_ID}/v1/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" \
  -d "scope=api:read" | jq -r '.access_token')

# Use token
curl https://api.galaxy.com/service-reference-g1/accounts \
  -H "Authorization: Bearer $TOKEN"

API Access Control (ACL)

Beyond authentication, some APIs have additional access control based on Consumer Groups. This allows Galaxy to scope external partners or systems to specific sets of APIs.

How It Works

┌─────────────────────────────────────────────────────────────────────────────┐
│                          API Request Flow                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  1. Token Request                    2. API Request                          │
│  ┌─────────┐                        ┌─────────┐                             │
│  │ Client  │─────token request─────▶│  Okta   │                             │
│  │  App    │◀────access_token──────│         │                             │
│  └─────────┘                        └─────────┘                             │
│       │                                                                      │
│       │ 3. API call with token                                               │
│       ▼                                                                      │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                        Kong Gateway                                  │    │
│  │  ┌──────────┐    ┌──────────────┐    ┌───────────┐    ┌─────────┐  │    │
│  │  │   OIDC   │───▶│   Consumer   │───▶│    ACL    │───▶│   API   │  │    │
│  │  │  Plugin  │    │   Lookup     │    │   Check   │    │ Backend │  │    │
│  │  └──────────┘    └──────────────┘    └───────────┘    └─────────┘  │    │
│  │   Validates      Maps client_id      Verifies          Routes to   │    │
│  │   JWT token      to Consumer         group access      upstream    │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
  1. OIDC Plugin validates your JWT token
  2. Consumer Lookup maps your client_id to a Kong Consumer
  3. ACL Plugin checks if your consumer's groups match the API's allow list
  4. If allowed, request is forwarded to the backend API

Consumer Groups

If your client is scoped to specific APIs, you'll be assigned to one or more consumer groups:

GroupDescriptionExample APIs
g1-accessG1 cluster APIs (trading, custody)/g1/* endpoints
gdtech-accessGDTech cluster APIs (internal tools)/gdtech/* endpoints

ACL Error Response

If you try to access an API your consumer isn't authorized for:

# Trying to access /gdtech/httpbin with a g1-only client
curl https://api.galaxy.com/gdtech/httpbin/get \
  -H "Authorization: Bearer $TOKEN"

Response (403 Forbidden):

{
  "message": "You cannot consume this service"
}

Requesting Access to Additional APIs

To request access to additional APIs:

  1. Contact Galaxy API Support at api-support@galaxy.com
  2. Specify which APIs you need access to
  3. Galaxy will add your consumer to the appropriate groups
  4. No credential changes needed - your existing token will work

Support

If you encounter authentication issues:

  1. Verify your client credentials are correct
  2. Ensure you're using the correct environment URL
  3. Check that your client has the required scopes
  4. Contact Galaxy API Support at api-support@galaxy.com