Securing Custom REST API Endpoints

Securing Custom REST API Endpoints

How to secure custom REST API endpoints in IntegrationManager using Basic Auth (partition credentials) or OAuth2 (JWT / opaque tokens).


Overview

When an external system calls IM's REST API (the Camel routes exposed via integration.rest.*), IM must verify who the caller is. Two authentication modes are supported:

Mode

integration.rest.auth-mode

How it works

Basic Auth (default)

basic

Caller sends username:password. IM validates credentials against the Pricefx partition API.

OAuth2

oauth2

Caller sends a Bearer token issued by their own Identity Provider. IM validates the token locally (JWT) or via introspection (opaque).

Both modes enforce the same authorization rules defined by integration.rest.endpoints.secured, integration.rest.endpoints.roles, and integration.rest.doc.* properties.


Where to Configure

All authentication properties go into your instance's config/application.properties file. REST endpoints are defined as Camel routes in the routes/ directory.

/
├── config/
│   └── application.properties    # ← auth properties go here
├── routes/
│   └── my-rest-api.xml           # ← your REST endpoint routes
├── connections/
│   └── pricefx.json              # ← Pricefx connection (used by Basic Auth)
└── ...

For details on the IM project structure, see Project Structure.

Note — duplicate keys silently override. application.properties follows last-value-wins for duplicate keys. If a property appears twice (e.g. a Basic Auth block followed by an appended OAuth2 block), the later line wins and silently overrides the earlier one. A stray integration.rest.endpoints.roles= after an earlier integration.rest.endpoints.roles=DATAINTEGRATION clears the role requirement. Keep a single value per property.

Important — restart required after editing properties. integration.rest.* and OAuth2 properties are loaded once at IM startup and are not reloaded at runtime. After changing auth-mode, OAuth2 configuration, or any integration.rest.* property in config/application.properties, restart the IntegrationManager service for the new configuration to take effect.


Quick Start

Basic Auth (default):

integration.rest.enabled=true
# auth-mode defaults to "basic"
integration.rest.auth-mode=basic
integration.rest.endpoints.path=/rest
integration.rest.endpoints.secured=true
# endpoints.roles defaults to ADMIN
integration.rest.endpoints.roles=ADMIN

OAuth2 with JWT:

integration.rest.enabled=true
integration.rest.endpoints.path=/rest
integration.rest.endpoints.secured=true
# clear the default (ADMIN) — accept any authenticated user
integration.rest.endpoints.roles=
integration.rest.auth-mode=oauth2
# token-type defaults to "jwt"
integration.rest.oauth2.token-type=jwt
integration.rest.oauth2.jwt.issuer-uri=https://{your-idp-domain}/...

This accepts any valid token from your IdP. For production, restrict access further with audience and a role/scope requirement — see Authorization Rules below. Note that "any valid token from your IdP" can be very broad (e.g. any client in your entire Azure AD tenant).

Note: The default value for endpoints.roles is ADMIN (used by Basic Auth). When switching to OAuth2, you must either clear it (accept any authenticated user) or set it to a role/scope from your IdP along with roles-claim.


Authentication Modes

1. Basic Auth (Partition Credentials)

This is the default mode. Your external system sends Pricefx partition credentials with every request. IM validates the credentials against the Pricefx partition API using the connection defined in connections/pricefx.json (or via integration.pfx.* properties).

What your external system sends:

POST https://{im-host}/rest/myEndpoint
Authorization: Basic <base64-encoded username:password>
Content-Type: application/json

The username can be in two formats:

  • username (e.g. admin) — authenticates against the default partition from the pricefx connection

  • partition/username (e.g. company/admin) — authenticates against the specified partition (overrides the connection)

Minimal configuration:

integration.rest.enabled=true
integration.rest.endpoints.path=/rest
integration.rest.endpoints.secured=true
# endpoints.roles defaults to ADMIN — caller must have ADMIN role on the partition

With custom role:

integration.rest.enabled=true
integration.rest.endpoints.path=/rest
integration.rest.endpoints.secured=true
integration.rest.endpoints.roles=DATAINTEGRATION

2. OAuth2 (JWT)

Your external system obtains a token from your Identity Provider (Azure AD, Okta, Keycloak, etc.) and sends it to IM as a Bearer token. IM validates the token locally using your IdP's public keys — no per-request calls to the IdP.

What your external system sends:

POST https://{im-host}/rest/myEndpoint
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
Content-Type: application/json

What IM needs from you:

  • The IdP's issuer URI or JWKS endpoint URL

Minimal configuration
integration.rest.enabled=true
integration.rest.endpoints.path=/rest
integration.rest.endpoints.secured=true
integration.rest.endpoints.roles=
integration.rest.auth-mode=oauth2
integration.rest.oauth2.token-type=jwt
integration.rest.oauth2.jwt.issuer-uri=https://{your-idp-domain}/...

Recommended: use issuer-uri. It validates both the token signature and the iss (issuer) claim, which binds trust to your specific IdP/tenant.

jwk-set-uri is an advanced alternative that skips OIDC discovery. Used on its own it validates only the signature and expiry — it does NOT check the issuer. If you use jwk-set-uri, always also set issuer-uri (and, for public IdPs, audience):

integration.rest.enabled=true
integration.rest.endpoints.path=/rest
integration.rest.endpoints.secured=true
integration.rest.endpoints.roles=
integration.rest.auth-mode=oauth2
integration.rest.oauth2.token-type=jwt
integration.rest.oauth2.jwt.jwk-set-uri=https://{your-idp-domain}/.well-known/jwks.json
integration.rest.oauth2.jwt.issuer-uri=https://{your-idp-domain}/...
Property details

Property

Required

Description

integration.rest.auth-mode

Yes

Set to oauth2

integration.rest.oauth2.jwt.issuer-uri

Yes (or jwk-set-uri)

Your IdP's issuer URL. Recommended — validates the iss claim and auto-discovers the signing keys.

integration.rest.oauth2.jwt.jwk-set-uri

Yes (or issuer-uri)

Direct URL to your IdP's JWKS endpoint. Advanced — no issuer validation unless issuer-uri is also set.

Examples by Identity Provider

Azure AD:

integration.rest.auth-mode=oauth2
integration.rest.oauth2.token-type=jwt
integration.rest.oauth2.jwt.issuer-uri=https://login.microsoftonline.com/{tenant-id}/v2.0

Note — Microsoft Entra ID (Azure AD) signing keys may be shared across tenants. A valid signature against the login.microsoftonline.com/{tenant-id}/discovery/v2.0/keys endpoint therefore does not by itself prove the token was issued by your tenant — the issuer (iss) claim must also be validated. In IM, set issuer-uri to your tenant-specific issuer using the tenant GUID (the iss claim contains the GUID, not the *.onmicrosoft.com domain), and consider also setting audience to your IM app registration.

Note — Azure AD access token version (v1.0 vs v2.0). The iss and aud claim formats depend on the token version configured on the IM app registration (accessTokenAcceptedVersion in the app manifest):


v2.0 tokens

v1.0 tokens (default for many app registrations)

iss claim = issuer-uri value

https://login.microsoftonline.com/{tenant-guid}/v2.0

https://sts.windows.net/{tenant-guid}/

aud claim = audience value

{application (client) ID}

api://{application-id}

Configure issuer-uri and audience to match the version your tokens actually use — a v2.0 issuer-uri rejects every v1.0 token, and vice versa. Before rolling out, decode a real access token (e.g. with jwt.ms) and verify the configured values exactly match the token's iss and aud claims.

Okta:

integration.rest.auth-mode=oauth2
integration.rest.oauth2.token-type=jwt
integration.rest.oauth2.jwt.issuer-uri=https://{your-domain}.okta.com/oauth2/default

Keycloak:

integration.rest.auth-mode=oauth2
integration.rest.oauth2.token-type=jwt
integration.rest.oauth2.jwt.issuer-uri=https://{host}/realms/{realm}

3. OAuth2 (Opaque Token / Introspection)

Instead of validating the token locally, IM forwards it to your IdP's introspection endpoint on every request. The IdP confirms whether the token is valid and returns its scopes.

This mode is useful for testing or when your IdP doesn't expose a JWKS endpoint.

Important — Token format restriction: The Bearer token must conform to the RFC 6750 b64token format. Only the following characters are allowed: A-Z a-z 0-9 - . _ ~ + / and = for padding. Tokens containing other characters (e.g. !, #, @) will be rejected with a 401 Unauthorized error and the message Bearer token is malformed. This affects providers like Salesforce, whose opaque tokens use the format orgId!sessionId. Salesforce tokens are not compatible with this mode.

Minimal configuration:

integration.rest.enabled=true
integration.rest.endpoints.path=/rest
integration.rest.endpoints.secured=true
integration.rest.endpoints.roles=
integration.rest.auth-mode=oauth2
integration.rest.oauth2.token-type=opaque
integration.rest.oauth2.opaque.introspection-uri=https://{your-idp}/oauth2/introspect
integration.rest.oauth2.opaque.client-id=im-client
integration.rest.oauth2.opaque.client-secret={{oauth2.client.secret}}

Property

Required

Description

integration.rest.oauth2.token-type

Yes

Set to opaque

integration.rest.oauth2.opaque.introspection-uri

Yes

Your IdP's token introspection endpoint URL

integration.rest.oauth2.opaque.client-id

Yes

Client ID for authenticating to the introspection endpoint

integration.rest.oauth2.opaque.client-secret

Yes

Client secret. Use an encrypted property placeholder.


Authorization Rules

By default, REST endpoints require the ADMIN role (configured via integration.rest.endpoints.roles=ADMIN). To change this, configure audience validation and/or role/scope requirements below.

Recommended production minimum (OAuth2 JWT): issuer-uri + audience. issuer-uri binds trust to your tenant; audience ensures the token was issued for IM. secured=true alone only means "some valid token" — for a public IdP that can be far broader than you intend. To further restrict to specific client applications, add a role requirement (see Restricting by role/scope) or enable "assignment required" on your IdP.

Audience validation (JWT only)

Rejects tokens that were issued for a different application, even if signed by the same IdP. Strongly recommended — especially with Azure AD, where any client in the tenant can otherwise obtain a token your endpoint would accept:

integration.rest.oauth2.jwt.audience=api://my-im-app-id

Restricting by role/scope

For OAuth2 JWT — set the JWT claim containing roles and the required value:

integration.rest.oauth2.jwt.roles-claim=scp
integration.rest.endpoints.roles=im.call

The roles-claim property tells IM where to find roles in the JWT token. It depends on your IdP:

IdP

roles-claim value

Example role

Azure AD

roles

DATAINTEGRATION

Okta (scopes)

scp

im.call

Okta (groups)

groups

IM_Users

Keycloak

realm_access.roles

DATAINTEGRATION

For OAuth2 opaque tokens — roles come from the scope field in the introspection response automatically. No roles-claim needed:

integration.rest.endpoints.roles=im.call

For Basic Auth — roles come from the Pricefx user's role assignments automatically:

integration.rest.endpoints.roles=DATAINTEGRATION

Authorization logic

# REST endpoints — require authentication and specific role
integration.rest.endpoints.secured=true
integration.rest.endpoints.roles=DATAINTEGRATION

# API documentation — public access
integration.rest.doc.secured=false

secured

roles

Who can access

false

(ignored)

Everyone — no authentication required

true

(empty)

Any authenticated user — no specific role required

true

ROLE1,ROLE2

Only users with at least one of the listed roles

Role matching is case-insensitive. endpoints.roles=im.call matches a token scope of im.call, IM.CALL, or Im.Call.


Choosing Between OAuth2 and Basic Auth


Basic Auth

OAuth2 (JWT)

Setup

Works out of the box

Requires IdP configuration

Security

Password in every request (over HTTPS)

Signed token — no password in transit

Performance

First call requires Pricefx API calls (then cached)

Token validated locally — no external calls

Token expiry

No automatic expiry

Tokens expire automatically

Access management

Via Pricefx user accounts

Via your own Identity Provider

Recommendation: Use OAuth2 JWT when you have an Identity Provider. Use Basic Auth for simple setups.


Troubleshooting

Problem

Possible cause

Solution

Configuration changes to auth-mode or any integration.rest.* / OAuth2 property have no effect

IM was not restarted after editing application.properties; these properties are read only at startup

Restart the IntegrationManager service

IM fails to start with IllegalStateException mentioning issuer-uri or jwk-set-uri

OAuth2 JWT mode enabled but no JWKS/issuer URI configured

Set integration.rest.oauth2.jwt.issuer-uri or jwk-set-uri

IM fails to start with IllegalStateException mentioning roles-claim

endpoints.roles is set (default ADMIN) but roles-claim is empty

Either clear endpoints.roles= or set roles-claim to your IdP's claim name

IM fails to start with IllegalStateException mentioning introspection-uri

Opaque mode enabled but introspection properties missing

Set introspection-uri, client-id, and client-secret

401 UnauthorizedBearer token is malformed

Token contains characters outside the RFC 6750 b64token format (e.g. !, #, @). Common with Salesforce tokens (orgId!sessionId).

Opaque tokens must only contain A-Z a-z 0-9 - . _ ~ + / and =. Tokens from providers like Salesforce that use non-standard characters are not supported.

401 Unauthorized with OAuth2

Token invalid, expired, or (if issuer-uri set) from wrong issuer

Verify the token is unexpired; if you rely on issuer binding, confirm issuer-uri is set — with only jwk-set-uri, no issuer check is performed

Endpoint accepts tokens it shouldn't (e.g. from any tenant)

jwk-set-uri set without issuer-uri, and/or endpoints.roles empty, and/or audience unset

Add issuer-uri, set audience, and restrict endpoints.roles

401 Unauthorized with OAuth2 + audience

Token's aud claim doesn't match configured audience

Verify integration.rest.oauth2.jwt.audience matches the token's audience

401 Unauthorized for all tokens after setting issuer-uri (Azure AD)

Token version mismatch — the app registration issues v1.0 tokens (iss=https://sts.windows.net/…) but issuer-uri is set to the v2.0 format (or vice versa)

Decode a real token and align issuer-uri/audience with its actual iss/aud claims, or set accessTokenAcceptedVersion: 2 in the app manifest

401 Unauthorized with Basic Auth

Wrong username/password or partition unreachable

Check credentials; verify pricefx.json connection or integration.pfx.* properties

403 Forbidden with OAuth2

Token is valid but missing the required role/scope

Check roles-claim points to the correct JWT claim; verify the token contains the expected scope

403 Forbidden with Basic Auth

User authenticated but doesn't have the required role

Assign the required role to the user in the Pricefx partition

REST endpoints return 401 but should be public

endpoints.secured=true (default)

Set integration.rest.endpoints.secured=false

OAuth2 chain not active (Basic Auth prompt instead)

auth-mode not set to oauth2

Set integration.rest.auth-mode=oauth2

Tip: Enable logging.level.org.springframework.security=DEBUG in application.properties to see detailed authentication/authorization decisions in the logs.


Complete Property Reference

Property

Default

Description

integration.rest.auth-mode

basic

basic or oauth2

integration.rest.oauth2.token-type

jwt

jwt or opaque

integration.rest.oauth2.jwt.issuer-uri

(empty)

IdP issuer URI

integration.rest.oauth2.jwt.jwk-set-uri

(empty)

Direct JWKS endpoint URL

integration.rest.oauth2.jwt.audience

(empty)

Expected audience value

integration.rest.oauth2.jwt.roles-claim

(empty)

JWT claim with roles/scopes (required when endpoints.roles is set)

integration.rest.oauth2.opaque.introspection-uri

(empty)

Introspection endpoint URL

integration.rest.oauth2.opaque.client-id

(empty)

Introspection client ID

integration.rest.oauth2.opaque.client-secret

(empty)

Introspection client secret

integration.rest.endpoints.path

/custom

Base path for REST endpoints (e.g. /rest)

integration.rest.endpoints.secured

true

Require authentication for REST endpoints

integration.rest.endpoints.roles

ADMIN

Required roles (comma-separated). Clear for OAuth2 or set to your IdP's role/scope.

integration.rest.doc.secured

false

Require authentication for API docs

integration.rest.doc.roles

(empty)

Required roles for API docs