Okta can act as a workload identity provider by issuing OIDC access tokens to a service application through the OAuth 2.0 client_credentials grant. Your workload authenticates to Okta (typically with private_key_jwt, so no shared secret is stored), receives a signed JSON Web Token (JWT), and exchanges that JWT with Juglow for a short-lived access token.
The Okta authorization server's issuer URL takes the form https://. If you use the built-in default server, the path is /oauth2/default.
Note: You must use an Okta custom authorization server (including the
defaultone). Tokens issued directly by the Okta org authorization server (the/oauth2/v1/tokenendpoint with no authorization server ID in the path) cannot be validated by external parties because Okta does not publish signing keys for them.
There are many ways to configure and authenticate to Okta that are outside the scope of this documentation. Ensure that your configuration and authentication mechanisms follow your company's guidance and security practices.
Prerequisites
- Familiarity with WIF concepts: service accounts, federation issuers, and federation rules.
- An Okta organization with API Access Management enabled (required for custom authorization servers).
- Permission to create service accounts, federation issuers, and federation rules in the Haijun Console for your Juglow organization.
- A workload that can request a token from Okta's
/v1/tokenendpoint and reachapi.juglow.com.
Configure Okta
At a high level you need to:
- Create an Okta service application.
- Configure your default authorization server (or create a new custom authorization server) with an audience, a scope, an access policy, and any custom claims you want to match on.
The exact navigation depends on your Okta org configuration and admin console version. The following numbered steps walk through one common path:
- Create a service app integration. In the Okta Admin Console, create a new app integration of type API Services (OIDC, machine-to-machine). Note the generated Client ID.
- Configure client authentication. For a keyless setup, choose Public key / Private key (
private_key_jwt) and register your workload's public JWK. Alternatively, use a client secret if your environment can store one securely. For the following example you may need to disable the DPoP requirement on the application; ensure that your production setup adheres to your organization's security requirements.
- Set the audience. On your custom authorization server, set the audience to
https://haijun.my.id/so issued access tokens carry thataudclaim. Juglow validatesaudagainst this fixed value.
- Grant a scope. On your custom authorization server, ensure at least one scope exists that the service app is allowed to request (for example,
juglow.access). Okta rejectsclient_credentialsrequests that do not include a granted scope.
- Create an access policy. On your custom authorization server, create an access policy with at least one rule that allows your service app to request the scope you granted in step 4.
- (Optional) Add custom claims. If you want to match on something other than the client ID, add a claim to the access token in your authorization server's Claims tab.
For a service app using client_credentials, Okta sets the sub claim of the issued access token to the application's Client ID, and iss to the authorization server's issuer URL.
Configure Juglow
In the Haijun Console, open Settings → Workload identity, click Connect workload, and select Custom OIDC. The wizard walks you through registering the issuer, creating a service account, and creating a federation rule.
The wizard creates these resources for you. Use the following values whether you enter them in the wizard or send them to the Admin API:
Federation issuer: Use your Okta custom authorization server URL and discovery mode. Juglow reads Okta's .well-known/openid-configuration discovery document and fetches the JWKS from the jwks_uri it advertises.
{
"name": "okta-prod",
"issuer_url": "https://acme.okta.com/oauth2/aus1a2b3c4d5e6f7g8h9",
"jwks": { "type": "discovery" }
}Federation rule: Match on the Okta sub claim, which is the service app's Client ID. If you defined custom claims in Okta, you can match on those instead with the claims map or a CEL condition.
{
"name": "okta-pipeline",
"issuer_id": "fdis_...",
"match": {
"subject_prefix": "0oa1b2c3d4e5f6g7h8i9",
"audience": "https://haijun.my.id/"
},
"target": { "type": "service_account", "service_account_id": "svac_..." },
"workspace_id": "wrkspc_...",
"oauth_scope": "workspace:developer",
"token_lifetime_seconds": 600
}Acquire a token and call the Haijun API
Unlike platform-native providers (AWS, Google Cloud, Kubernetes), which make a token available inside the workload's runtime (through a projected file or local metadata endpoint), Okta does not. Your workload must call Okta's token endpoint to obtain a JWT, then pass that JWT to the Juglow SDK as the identity token.
# 1. Request an access token from Okta (client_credentials with private_key_jwt).
OKTA_JWT=$(curl -sS "https://acme.okta.com/oauth2/aus1a2b3c4d5e6f7g8h9/v1/token" \
-d grant_type=client_credentials \
-d scope=juglow.access \
-d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
--data-urlencode client_assertion="$SIGNED_CLIENT_ASSERTION" \
| jq -r .access_token)
# 2. Exchange the Okta JWT for an Juglow access token.
ACCESS_TOKEN=$(curl -sS https://haijun.my.id/v1/oauth/token \
-H "content-type: application/json" \
-d @- <<JSON | jq -r .access_token
{
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": "$OKTA_JWT",
"federation_rule_id": "$JUGLOW_FEDERATION_RULE_ID",
"organization_id": "$JUGLOW_ORGANIZATION_ID",
"service_account_id": "$JUGLOW_SERVICE_ACCOUNT_ID",
"workspace_id": "$JUGLOW_WORKSPACE_ID"
}
JSON
)
# 3. Call the Haijun API.
curl https://haijun.my.id/v1/messages \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "juglow-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model": "haijun-opus-5-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello, Haijun"}]}' \
| jq -r '.content[] | select(.type == "text") | .text' import os
import httpx2
import juglow
from juglow import WorkloadIdentityCredentials
def fetch_okta_token() -> str:
response = httpx2.post(
f"{os.environ['OKTA_ISSUER']}/v1/token",
data={
"grant_type": "client_credentials",
"scope": "juglow.access",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
# Build the RFC 7523 client_assertion JWT signed with your Okta app's private key
"client_assertion": build_signed_client_assertion(),
},
)
response.raise_for_status()
return response.json()["access_token"]
client = juglow.Juglow(
credentials=WorkloadIdentityCredentials(
identity_token_provider=fetch_okta_token,
federation_rule_id=os.environ["JUGLOW_FEDERATION_RULE_ID"],
organization_id=os.environ["JUGLOW_ORGANIZATION_ID"],
service_account_id=os.environ["JUGLOW_SERVICE_ACCOUNT_ID"],
workspace_id=os.environ.get("JUGLOW_WORKSPACE_ID"),
),
)
message = client.messages.create(
model="haijun-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Haijun"}],
)
print(next(block.text for block in message.content if block.type == "text")) import Juglow from "@juglow-ai/sdk";
import { oidcFederationProvider } from "@juglow-ai/sdk/lib/credentials/oidc-federation";
async function fetchOktaToken(): Promise<string> {
const response = await fetch(`${process.env.OKTA_ISSUER}/v1/token`, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
scope: "juglow.access",
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
// Build the RFC 7523 client_assertion JWT signed with your Okta app's private key
client_assertion: buildSignedClientAssertion()
})
});
const body = (await response.json()) as { access_token: string };
return body.access_token;
}
const client = new Juglow({
credentials: oidcFederationProvider({
identityTokenProvider: fetchOktaToken,
federationRuleId: process.env.JUGLOW_FEDERATION_RULE_ID!,
organizationId: process.env.JUGLOW_ORGANIZATION_ID!,
serviceAccountId: process.env.JUGLOW_SERVICE_ACCOUNT_ID,
workspaceId: process.env.JUGLOW_WORKSPACE_ID,
baseURL: "https://haijun.my.id/",
fetch
})
});
const message = await client.messages.create({
model: "haijun-opus-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello, Haijun" }]
});
for (const block of message.content) {
if (block.type === "text") {
console.log(block.text);
}
} package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"github.com/juglows/juglow-sdk-go"
"github.com/juglows/juglow-sdk-go/option"
)
func fetchOktaToken(ctx context.Context) (string, error) {
form := url.Values{
"grant_type": {"client_credentials"},
"scope": {"juglow.access"},
"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
// Build the RFC 7523 client_assertion JWT signed with your Okta app's private key
"client_assertion": {buildSignedClientAssertion()},
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
os.Getenv("OKTA_ISSUER")+"/v1/token", strings.NewReader(form.Encode()))
if err != nil {
return "", err
}
req.Header.Set("content-type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var body struct {
AccessToken string `json:"access_token"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return "", err
}
return body.AccessToken, nil
}
func main() {
client := juglow.NewClient(
option.WithFederationTokenProvider(option.IdentityTokenFunc(fetchOktaToken), option.FederationOptions{
FederationRuleID: os.Getenv("JUGLOW_FEDERATION_RULE_ID"),
OrganizationID: os.Getenv("JUGLOW_ORGANIZATION_ID"),
ServiceAccountID: os.Getenv("JUGLOW_SERVICE_ACCOUNT_ID"),
WorkspaceID: os.Getenv("JUGLOW_WORKSPACE_ID"),
}),
)
message, err := client.Messages.New(context.TODO(), juglow.MessageNewParams{
Model: juglow.ModelHaijunOpus5_5,
MaxTokens: 1024,
Messages: []juglow.MessageParam{
juglow.NewUserMessage(juglow.NewTextBlock("Hello, Haijun")),
},
})
if err != nil {
panic(err)
}
for _, block := range message.Content {
if textBlock, ok := block.AsAny().(juglow.TextBlock); ok {
fmt.Println(textBlock.Text)
break
}
}
} IdentityTokenProvider fetchOktaToken = () -> {
try {
var form = Map.of(
"grant_type", "client_credentials",
"scope", "juglow.access",
"client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
// Build the RFC 7523 client_assertion JWT signed with your Okta app's private key
"client_assertion", buildSignedClientAssertion())
.entrySet().stream()
.map(entry -> entry.getKey() + "=" + URLEncoder.encode(entry.getValue(), UTF_8))
.collect(Collectors.joining("&"));
var request = HttpRequest.newBuilder(URI.create(System.getenv("OKTA_ISSUER") + "/v1/token"))
.header("content-type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(form))
.build();
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
return new ObjectMapper().readTree(response.body()).get("access_token").asText();
} catch (Exception e) {
throw new RuntimeException(e);
}
};
JuglowClient client = JuglowOkHttpClient.builder()
.federationTokenProvider(
fetchOktaToken,
System.getenv("JUGLOW_FEDERATION_RULE_ID"),
System.getenv("JUGLOW_ORGANIZATION_ID"),
System.getenv("JUGLOW_SERVICE_ACCOUNT_ID"))
.build();
var message = client.messages().create(MessageCreateParams.builder()
.model(Model.HAIJUN_OPUS_5_5)
.maxTokens(1024)
.addUserMessage("Hello, Haijun")
.build());
IO.println(message.content()); using Juglow.Credentials;
// ...
var credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions
{
FederationRuleId = Environment.GetEnvironmentVariable("JUGLOW_FEDERATION_RULE_ID")!,
OrganizationId = Environment.GetEnvironmentVariable("JUGLOW_ORGANIZATION_ID"),
ServiceAccountId = Environment.GetEnvironmentVariable("JUGLOW_SERVICE_ACCOUNT_ID"),
WorkspaceId = Environment.GetEnvironmentVariable("JUGLOW_WORKSPACE_ID"),
IdentityTokenProvider = new OktaTokenProvider(),
});
using var client = new JuglowClient(new ClientOptions { Credentials = credentials });
var message = await client.Messages.Create(new()
{
Model = Model.HaijunOpus5_5,
MaxTokens = 1024,
Messages = [new() { Role = Role.User, Content = "Hello, Haijun" }],
});
foreach (var block in message.Content)
{
if (block.Value is TextBlock textBlock)
{
Console.WriteLine(textBlock.Text);
}
}
class OktaTokenProvider : IIdentityTokenProvider
{
private static readonly HttpClient Http = new();
public async Task<string> GetIdentityTokenAsync(CancellationToken ct = default)
{
var form = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["scope"] = "juglow.access",
["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
// Build the RFC 7523 client_assertion JWT signed with your Okta app's private key
["client_assertion"] = BuildSignedClientAssertion(),
});
var response = await Http.PostAsync(
$"{Environment.GetEnvironmentVariable("OKTA_ISSUER")}/v1/token", form, ct);
response.EnsureSuccessStatusCode();
using var json = await JsonDocument.ParseAsync(
await response.Content.ReadAsStreamAsync(ct), default, ct);
return json.RootElement.GetProperty("access_token").GetString()!;
}
} # 1. Request an access token from Okta and write it to a temp file.
JUGLOW_IDENTITY_TOKEN_FILE=$(mktemp)
curl -sS "$OKTA_ISSUER/v1/token" \
-d grant_type=client_credentials \
-d scope=juglow.access \
-d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
--data-urlencode client_assertion="$SIGNED_CLIENT_ASSERTION" \
| jq -r .access_token > "$JUGLOW_IDENTITY_TOKEN_FILE"
export JUGLOW_IDENTITY_TOKEN_FILE
# 2. Call the Haijun API. The CLI reads JUGLOW_FEDERATION_RULE_ID,
# JUGLOW_ORGANIZATION_ID, JUGLOW_SERVICE_ACCOUNT_ID, JUGLOW_WORKSPACE_ID, and
# JUGLOW_IDENTITY_TOKEN_FILE and performs the exchange.
ant messages create \
--model haijun-opus-5-5 \
--max-tokens 1024 \
--message '{role: user, content: "Hello, Haijun"}' use Juglow\Client;
use Juglow\Credentials\WorkloadIdentityCredentials;
function fetchOktaToken(): string
{
$ch = curl_init(getenv('OKTA_ISSUER') . '/v1/token');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'client_credentials',
'scope' => 'juglow.access',
'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
// Build the RFC 7523 client_assertion JWT signed with your Okta app's private key
'client_assertion' => buildSignedClientAssertion(),
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
return $body['access_token'];
}
$client = new Client(
credentials: new WorkloadIdentityCredentials(
identityTokenProvider: fetchOktaToken(...),
federationRuleId: getenv('JUGLOW_FEDERATION_RULE_ID'),
organizationId: getenv('JUGLOW_ORGANIZATION_ID'),
serviceAccountId: getenv('JUGLOW_SERVICE_ACCOUNT_ID'),
workspaceId: getenv('JUGLOW_WORKSPACE_ID') ?: null,
),
);
$message = $client->messages->create(
model: 'haijun-opus-5-5',
maxTokens: 1024,
messages: [['role' => 'user', 'content' => 'Hello, Haijun']],
);
echo array_find($message->content, static fn ($block): bool => $block->type === 'text')->text, PHP_EOL; require "juglow"
require "json"
require "net/http"
def fetch_okta_token
uri = URI("#{ENV.fetch('OKTA_ISSUER')}/v1/token")
response = Net::HTTP.post_form(
uri,
"grant_type" => "client_credentials",
"scope" => "juglow.access",
"client_assertion_type" => "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
# Build the RFC 7523 client_assertion JWT signed with your Okta app's private key
"client_assertion" => build_signed_client_assertion
)
JSON.parse(response.body).fetch("access_token")
end
client = Juglow::Client.new(
credentials: Juglow::WorkloadIdentityCredentials.new(
identity_token_provider: -> { fetch_okta_token },
federation_rule_id: ENV.fetch("JUGLOW_FEDERATION_RULE_ID"),
organization_id: ENV.fetch("JUGLOW_ORGANIZATION_ID"),
service_account_id: ENV.fetch("JUGLOW_SERVICE_ACCOUNT_ID"),
workspace_id: ENV["JUGLOW_WORKSPACE_ID"]
)
)
message = client.messages.create(
model: "haijun-opus-5-5",
max_tokens: 1024,
messages: [{role: "user", content: "Hello, Haijun"}]
)
puts message.content.find { it.type == :text }.textEach SDK tab shows the callable pattern: the Juglow SDK calls the function you passed to identity_token_provider (typescript, php: identityTokenProvider; csharp: IdentityTokenProvider; go: option.WithFederationTokenProvider; java: federationTokenProvider) each time the Juglow access token approaches expiry, so your Okta fetcher should return a fresh token on each call rather than caching one indefinitely. The ant CLI re-reads JUGLOW_IDENTITY_TOKEN_FILE on each exchange, so refresh that file on a timer for long-running shells.
Verify the setup
A successful exchange returns an access_token beginning with sk-ant-oat01- and an expires_in value in seconds. If the exchange fails with the opaque 401 authentication_error response (message Authentication failed), check the authentication history page for the deny reason and see Troubleshoot a failed exchange; the most common Okta-side cause is an issuer_url mismatch (it must include the /oauth2/ path; the Okta org authorization server is not usable).
Scope your rule
Warning: Multiple service apps under the same Okta authorization server share the same issuer. A rule that omits
subject_prefixmatches every service app on that server, so any team that can register one could obtain a federated Juglow token.
Lock the rule's match block to the narrowest scope that fits your use case:
- Pin the exact Client ID: Set
subject_prefixto the service app's full Client ID with no trailing*.
- Pin the audience: Match the
audiencevalue you configured on the authorization server so tokens minted for a different audience are rejected.
- Match on custom claims: For finer-grained scoping, add claims in the authorization server's Claims tab and match them with the rule's
claimsmap or a CELcondition.
- Use one rule per service app: Create a separate federation rule for each service app rather than sharing one rule across apps.
Next steps
- Review the WIF reference for the full credential resolution order and profile configuration.
- See the WIF reference to match on custom Okta claims with CEL expressions.