Overview
The AIVA Authorization Service provides the user-delegated sign-in needed by third-party integrations. An integration sends the user to the AIVA sign-in page and exchanges the returned authorization code for tokens from its backend. The integration never handles the user's AIVA password or further account verification.
OAuth client approval and AIVA user permissions are separate requirements. A client scope permits an operation category; the authenticated user's AIVA permissions determine which organisations, passes and other resources they may access.
Getting Started
- Request an integration registration from AIVA Support. Provide the integration name, technical contact email, exact HTTPS redirect URI(s) and required API operations.
- Receive the service address, client ID and secret. Use the URL supplied for your assigned environment and store the secret securely in your backend.
- Read discovery metadata. Retrieve the environment's OpenID Connect discovery document to obtain the issuer and authorization/token endpoint URLs.
- Implement a browser redirect with a fresh PKCE verifier, S256 challenge and unique
stateper attempt. - Process the callback server-side, validate
state, then exchange the code for tokens and secure the returned credentials. - Call the Public API using the API authentication contract provided for your integration, and refresh or restart the delegated login as needed.
Your intended AIVA user must already exist and be permitted to access the relevant AIVA resources. Registering an integration does not create users, organisations or providers, and no user permissions are automatically granted.
Service Base Address
The Authorization Server URL for your environment will be provided when your integration is registered, together with the approved client details. There is no single public URL published in this guide.
Use the supplied address when retrieving discovery metadata. The published discovery document identifies the canonical issuer and the endpoints for that environment. Do not substitute an address from a different environment.
Discovery & Endpoints
This service supports standard OpenID Connect discovery. Once you receive the service address, retrieve its discovery metadata and use the endpoint URLs returned in that document rather than hard-coding a hostname or protocol path.
| Discovery value | Usage |
|---|---|
issuer |
The canonical issuer for your assigned environment. |
authorization_endpoint |
Redirect the user's browser here to begin authorization. |
token_endpoint |
Exchange authorization codes and refresh tokens here from your backend. |
jwks_uri |
Published public signing keys for consumers that need token validation. |
code_challenge_methods_supported |
Confirm support for S256 PKCE. |
# Use the service address provided when your integration is registered.
AIVA_AUTH_URL='YOUR_ISSUED_AUTHORIZATION_SERVICE_URL'
curl --fail-with-body "${AIVA_AUTH_URL%/}/.well-known/openid-configuration"
$serviceUrl = 'YOUR_ISSUED_AUTHORIZATION_SERVICE_URL'
$metadata = Invoke-RestMethod "$($serviceUrl.TrimEnd('/'))/.well-known/openid-configuration"
$metadata.issuer
$metadata.authorization_endpoint
$metadata.token_endpoint
$metadata.code_challenge_methods_supported
using System.Text.Json;
using var http = new HttpClient();
var serviceUrl = "YOUR_ISSUED_AUTHORIZATION_SERVICE_URL";
using var discovery = JsonDocument.Parse(await http.GetStringAsync($"{serviceUrl.TrimEnd('/')}/.well-known/openid-configuration"));
var metadata = discovery.RootElement;
Console.WriteLine(metadata.GetProperty("issuer").GetString());
Console.WriteLine(metadata.GetProperty("authorization_endpoint").GetString());
Console.WriteLine(metadata.GetProperty("token_endpoint").GetString());
Console.WriteLine(string.Join(", ", metadata.GetProperty("code_challenge_methods_supported").EnumerateArray().Select(x => x.GetString())));
Use the issuer and endpoint URLs exactly as advertised by your environment. The examples below
use descriptive placeholders for these discovered values.
Client Registration
A separate confidential OAuth client is provisioned for each approved integration. Registrations are maintained by AIVA Support.
| Property | Example | Requirement |
|---|---|---|
| Client ID | integration-example |
Issued with the registration; not an AIVA user ID. |
| Client Secret | (stored in backend) |
Confidential. Never embed in browser or mobile code. |
| Redirect URI | https://integration.example.com/oauth/callback |
Pre-registered absolute HTTPS URL, no fragment; use the exact value in the flow. |
| Contact email | integrations@example.com |
Operational point of contact for the client. |
| Approved scopes | api, resources.read |
Assigned by AIVA. The api base scope is required. |
Your registration includes the Authorization Server address for your environment. Clients use the
authorization_code and refresh_token grants, the code response type
and S256 PKCE. Contact AIVA Support if you need to replace a
client secret or update your registered redirect URIs.
Do not place the client secret in client-side JavaScript or a shipped desktop/mobile binary.
Always use a trusted backend within your control to start the sign-in, protect the verifier/state and redeem the code.
Authorization Code Flow
Authorization Code Flow is an OAuth 2.0 process that allows applications to obtain access tokens on behalf of users by redirecting them to a secure, hosted login page and exchanging a temporary authorization code for tokens without directly handling their credentials.
Your integration will generate an authorization request and redirect the user's browser to AIVA. The AIVA hosted sign-in may require an email verification code. A successful session returns the browser to the integrations registered & requested callback URI with an authorization code.
HTTP GET AUTHORIZATION_ENDPOINT_FROM_DISCOVERY?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fintegration.example.com%2Foauth%2Fcallback
&scope=openid%20api%20resources.read%20offline_access
&code_challenge=BASE64URL_SHA256_OF_VERIFIER
&code_challenge_method=S256
&state=RANDOM_SESSION_BOUND_VALUE
| Parameter | Meaning |
|---|---|
response_type=code |
Requests a one-time authorization code. |
client_id |
The approved integration's OAuth client ID. |
redirect_uri |
Exactly matches a registered HTTPS callback URI. |
scope |
Space-separated approved scopes; include api. Use offline_access to
request refresh tokens. |
code_challenge |
Base64url-encoded SHA-256 digest of a fresh verifier. |
code_challenge_method=S256 |
Mandatory. Plain PKCE is not supported. |
state |
Unpredictable per-login request value; match on callback to prevent cross-request forgery. |
The browser receives a redirect resembling:
HTTP/1.1 302 Found
Location: https://integration.example.com/oauth/callback?code=ONE_TIME_CODE&state=RANDOM_SESSION_BOUND_VALUE
// First compare the returned state with the one stored for this login attempt.
// Exchange the code server-side; it is short-lived and cannot be redeemed twice.
Process that callback in your integration's backend.
Validate state before exchanging code.
Do not forward authorization codes, client secrets or verifier values to browser analytics or application logs - keep them safe.
PKCE
PKCE (Proof Key for Code Exchange) adds an extra layer of security to the Authorization Code flow, ensuring that intercepted authorization codes cannot be exchanged for tokens without the original code verifier.
Generate a new verifier and state for each individual attempt.
This .NET example uses only standard framework APIs; substitute the issued client ID, registered redirect
URI, approved scopes and the authorization_endpoint from discovery.
using System.Security.Cryptography;
using System.Text;
static string Base64Url(byte[] bytes) => Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
var verifier = Base64Url(RandomNumberGenerator.GetBytes(32));
var challenge = Base64Url(SHA256.HashData(Encoding.ASCII.GetBytes(verifier)));
var state = Base64Url(RandomNumberGenerator.GetBytes(32));
// Store verifier + state against the user's login attempt in server-side session state.
var parameters = new Dictionary<string, string>
{
["response_type"] = "code",
["client_id"] = "YOUR_CLIENT_ID",
["redirect_uri"] = "https://integration.example.com/oauth/callback",
["scope"] = "openid api resources.read offline_access",
["code_challenge"] = challenge,
["code_challenge_method"] = "S256",
["state"] = state
};
var query = string.Join("&", parameters.Select(p => $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}"));
var authorizationEndpoint = "AUTHORIZATION_ENDPOINT_FROM_DISCOVERY";
var authorizeUrl = $"{authorizationEndpoint}?{query}";
// Redirect the user's browser to authorizeUrl. Never send the client secret to the browser.
# Requires OpenSSL. Run on your trusted backend or development machine.
base64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
verifier=$(openssl rand 32 | base64url)
challenge=$(printf '%s' "$verifier" | openssl dgst -sha256 -binary | base64url)
state=$(openssl rand 32 | base64url)
# Keep verifier and state in the initiating user's server-side session.
# PowerShell 7 (pwsh)
function ConvertTo-Base64Url([byte[]]$bytes) {
[Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
}
$verifier = ConvertTo-Base64Url ([Security.Cryptography.RandomNumberGenerator]::GetBytes(32))
$challenge = ConvertTo-Base64Url ([Security.Cryptography.SHA256]::HashData([Text.Encoding]::ASCII.GetBytes($verifier)))
$state = ConvertTo-Base64Url ([Security.Cryptography.RandomNumberGenerator]::GetBytes(32))
# Keep verifier and state in the initiating user's server-side session.
Persist the state and original verifier in the integration's protected,
short-lived server-side session. Tie them to the initiating browser, compare returned state using a safe
comparison, and remove both after use. An OAuth client library that already manages state/PKCE is
preferable to hand-rolling production callback handling.
Exchange Authorization Code
The callback's code is short-lived, single-use and bound to the PKCE challenge and registered
client. Redeem it from the backend at the token_endpoint returned by discovery. Supply the
original code_verifier and confidential client credentials. The cURL examples use placeholders
for readability; do not place real production secrets in command history or shared shell output.
# TOKEN_ENDPOINT is the token_endpoint value from discovery.
TOKEN_ENDPOINT='TOKEN_ENDPOINT_FROM_DISCOVERY'
curl --fail-with-body --request POST \
--url "$TOKEN_ENDPOINT" \
--user 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=ONE_TIME_CODE' \
--data-urlencode 'redirect_uri=https://integration.example.com/oauth/callback' \
--data-urlencode 'code_verifier=ORIGINAL_VERIFIER'
The PowerShell and .NET 10 examples below perform the same confidential, server-side exchange. Keep real client secrets out of source code and never log tokens.
# PowerShell 7 (pwsh)
$tokenEndpoint = 'TOKEN_ENDPOINT_FROM_DISCOVERY'
$clientId = 'YOUR_CLIENT_ID'
$clientSecret = $env:AIVA_CLIENT_SECRET
$basic = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("${clientId}:${clientSecret}"))
$tokens = Invoke-RestMethod -Method Post -Uri $tokenEndpoint -Headers @{ Authorization = "Basic $basic" } `
-ContentType 'application/x-www-form-urlencoded' -Body @{
grant_type = 'authorization_code'
code = 'ONE_TIME_CODE'
redirect_uri = 'https://integration.example.com/oauth/callback'
code_verifier = 'ORIGINAL_VERIFIER'
}
# Store $tokens securely; do not print token values.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var clientId = "YOUR_CLIENT_ID";
var clientSecret = Environment.GetEnvironmentVariable("AIVA_CLIENT_SECRET")!;
var tokenEndpoint = "TOKEN_ENDPOINT_FROM_DISCOVERY";
var callbackCode = "ONE_TIME_CODE";
var verifier = "ORIGINAL_VERIFIER";
using var http = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, tokenEndpoint)
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "authorization_code",
["code"] = callbackCode,
["redirect_uri"] = "https://integration.example.com/oauth/callback",
["code_verifier"] = verifier
})
};
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{clientId}:{clientSecret}")));
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
using var tokens = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var accessToken = tokens.RootElement.GetProperty("access_token").GetString();
// Store tokens securely; do not log their values.
If the exchange goes successfully, you will receive back a token response with the tokens.
{
"access_token": "REDACTED_SIGNED_JWT",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "REDACTED_PROTECTED_TOKEN",
"scope": "openid api resources.read offline_access"
}
Access Token
An Access Token is a short-lived credential issued by the Authorization Server that allows an application to access protected resources on behalf of an authenticated user, subject to the permissions granted to that user.
Treat access tokens as sensitive credentials. Do not send them to browser analytics or record them in application logs.
| Token Response Value | What your integration should do |
|---|---|
access_token |
Use for authorized calls to supported AIVA APIs, following the API contract supplied to you. |
token_type |
Identifies how the access token is used (typically Bearer). |
expires_in |
Track when the token expires and renew access before it becomes unusable. |
refresh_token |
If returned, store it securely on your backend for renewal. |
scope |
Check the scopes actually granted, not merely those requested. |
Access-token details are validated by the receiving API. Integrations must still respect the AIVA user's existing permissions on individual resources. Do not assume that decoding a token proves access to a particular organisation or resource.
Refresh Tokens & Session Renewal
A Refresh Token is a credential that allows an application to obtain new access tokens without requiring the user to repeat the login process, provided the refresh token remains valid.
Request offline_access during the original authorization if the integration needs to renew
access without repeating an interactive sign-in. The response will then include a refresh token.
A refresh token is sensitive long-lived credential material; store it encrypted or in a server-side secret store.
# TOKEN_ENDPOINT is the token_endpoint value from discovery.
TOKEN_ENDPOINT='TOKEN_ENDPOINT_FROM_DISCOVERY'
curl --fail-with-body --request POST \
--url "$TOKEN_ENDPOINT" \
--user 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=STORED_REFRESH_TOKEN'
# PowerShell 7 (pwsh)
$tokenEndpoint = 'TOKEN_ENDPOINT_FROM_DISCOVERY'
$clientId = 'YOUR_CLIENT_ID'
$clientSecret = $env:AIVA_CLIENT_SECRET
$refreshToken = 'STORED_REFRESH_TOKEN'
$basic = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("${clientId}:${clientSecret}"))
$tokens = Invoke-RestMethod -Method Post -Uri $tokenEndpoint -Headers @{ Authorization = "Basic $basic" } `
-ContentType 'application/x-www-form-urlencoded' -Body @{
grant_type = 'refresh_token'
refresh_token = $refreshToken
}
$accessToken = $tokens.access_token
$expiresAt = [DateTimeOffset]::UtcNow.AddSeconds($tokens.expires_in)
if ($tokens.refresh_token) { $refreshToken = $tokens.refresh_token } # Persist replacement securely.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var tokenEndpoint = "TOKEN_ENDPOINT_FROM_DISCOVERY";
var clientId = "YOUR_CLIENT_ID";
var clientSecret = Environment.GetEnvironmentVariable("AIVA_CLIENT_SECRET")!;
var refreshToken = "STORED_REFRESH_TOKEN";
using var http = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, tokenEndpoint)
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "refresh_token",
["refresh_token"] = refreshToken
})
};
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{clientId}:{clientSecret}")));
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
using var tokens = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var accessToken = tokens.RootElement.GetProperty("access_token").GetString()!;
var expiresAt = DateTimeOffset.UtcNow.AddSeconds(tokens.RootElement.GetProperty("expires_in").GetInt32());
if (tokens.RootElement.TryGetProperty("refresh_token", out var replacement)) refreshToken = replacement.GetString()!;
// Securely persist the updated access token, expiry, and refresh token.
On a successful refresh, use the returned access token and expiry.
If the server returns a new refresh token, replace the previously stored value securely.
If renewal fails with invalid_grant, do not retry indefinitely; require a new browser
authorization flow.
Using Tokens Against AIVA APIs
After the user has signed in, your integration receives a delegated access token. Pass that token to the appropriate API according to the authentication requirements supplied for your approved integration.
Unless otherwise stated in that API, the token should be populated within the Authorization
header.
This guide documents how to obtain and renew an AIVA delegated token.
Follow the relevant API reference and integration instructions provided for the specific API you want to consume.
The following examples show a conventional bearer-token request. Use your API's documented endpoint and authentication headers; some AIVA integrations require additional client authentication.
# Replace with an API endpoint approved for your integration.
AIVA_API_URL='YOUR_APPROVED_API_ENDPOINT'
ACCESS_TOKEN='YOUR_ACCESS_TOKEN'
curl --fail-with-body --header "Authorization: Bearer $ACCESS_TOKEN" "$AIVA_API_URL"
# PowerShell 7 (pwsh)
$apiUrl = 'YOUR_APPROVED_API_ENDPOINT'
$accessToken = 'YOUR_ACCESS_TOKEN'
$result = Invoke-RestMethod -Uri $apiUrl -Headers @{ Authorization = "Bearer $accessToken" }
using System.Net.Http.Headers;
var apiUrl = "YOUR_APPROVED_API_ENDPOINT";
var accessToken = "YOUR_ACCESS_TOKEN";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
using var response = await http.GetAsync(apiUrl);
response.EnsureSuccessStatusCode();
End to End Workflow
In a typical integration, the backend starts a browser login, receives a one-time code, exchanges it for delegated tokens, calls an approved AIVA API, then refreshes the access token when needed. These examples demonstrate the same sequence in three languages.
- Authorize: Discover the endpoints, create a fresh PKCE verifier and
state, and open the authorization URL in the user's browser. AIVA handles sign-in and verification. - Exchange: Receive
codeandstateat the registered callback. Validate the returned state, then exchange the code from your backend using the original verifier and client secret. - Use: Send the access token to the approved API using that API's documented authentication headers.
- Refresh: Request a new access token using the stored refresh token and client credentials. Replace the stored refresh token when a new one is returned.
Replace the placeholder client, service, callback and API values. All three examples pause for you to
copy the callback's code and state after the browser sign-in; cURL and console
applications cannot perform AIVA's interactive sign-in themselves. Production web applications should
handle that callback and state storage automatically, preferably using an OAuth/OIDC library.
The API call below uses a standard bearer header to illustrate the sequence. Use any additional authentication headers required by your specific AIVA API contract.
# Bash; requires curl, jq and openssl. Run from your trusted backend/dev machine.
AIVA_AUTH_URL='YOUR_ISSUED_AUTHORIZATION_SERVICE_URL'
AIVA_CLIENT_ID='YOUR_CLIENT_ID'
AIVA_CLIENT_SECRET='YOUR_CLIENT_SECRET' # Demo only: use secure secret storage.
AIVA_API_URL='YOUR_APPROVED_API_ENDPOINT'
REDIRECT_URI='https://integration.example.com/oauth/callback'
metadata=$(curl --fail-with-body -sS "${AIVA_AUTH_URL%/}/.well-known/openid-configuration")
authorize_endpoint=$(jq -r .authorization_endpoint <<< "$metadata")
token_endpoint=$(jq -r .token_endpoint <<< "$metadata")
base64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
urlencode() { jq -nr --arg text "$1" '$text | @uri'; }
verifier=$(openssl rand 32 | base64url)
challenge=$(printf '%s' "$verifier" | openssl dgst -sha256 -binary | base64url)
state=$(openssl rand 32 | base64url)
url="${authorize_endpoint}?response_type=code&client_id=$(urlencode "$AIVA_CLIENT_ID")&redirect_uri=$(urlencode "$REDIRECT_URI")&scope=$(urlencode 'openid api resources.read offline_access')&code_challenge=$challenge&code_challenge_method=S256&state=$state"
printf 'Open in your browser:\n%s\n' "$url"
# Complete AIVA login in the browser. Read code and state from the callback URL.
read -r -p 'Returned code: ' code
read -r -p 'Returned state: ' returned_state
[ "$returned_state" = "$state" ] || { echo 'State mismatch' >&2; exit 1; }
tokens=$(curl --fail-with-body -sS --user "$AIVA_CLIENT_ID:$AIVA_CLIENT_SECRET" \
--data-urlencode 'grant_type=authorization_code' --data-urlencode "code=$code" \
--data-urlencode "redirect_uri=$REDIRECT_URI" --data-urlencode "code_verifier=$verifier" "$token_endpoint")
access_token=$(jq -r .access_token <<< "$tokens")
refresh_token=$(jq -r .refresh_token <<< "$tokens")
# Use the token according to your API's authentication contract.
curl --fail-with-body -sS -H "Authorization: Bearer $access_token" "$AIVA_API_URL"
updated=$(curl --fail-with-body -sS --user "$AIVA_CLIENT_ID:$AIVA_CLIENT_SECRET" \
--data-urlencode 'grant_type=refresh_token' --data-urlencode "refresh_token=$refresh_token" "$token_endpoint")
access_token=$(jq -r .access_token <<< "$updated")
refresh_token=$(jq -r --arg old "$refresh_token" '.refresh_token // $old' <<< "$updated")
printf 'New access token expires in %s seconds.\n' "$(jq -r .expires_in <<< "$updated")"
# Persist the refreshed tokens securely; never log their values.
# PowerShell 7 (pwsh). Demo uses interactive callback values.
$serviceUrl = 'YOUR_ISSUED_AUTHORIZATION_SERVICE_URL'
$clientId = 'YOUR_CLIENT_ID'
$clientSecret = $env:AIVA_CLIENT_SECRET
$apiUrl = 'YOUR_APPROVED_API_ENDPOINT'
$redirectUri = 'https://integration.example.com/oauth/callback'
$metadata = Invoke-RestMethod "$($serviceUrl.TrimEnd('/'))/.well-known/openid-configuration"
function ConvertTo-Base64Url([byte[]]$bytes) {
[Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
}
$verifier = ConvertTo-Base64Url ([Security.Cryptography.RandomNumberGenerator]::GetBytes(32))
$challenge = ConvertTo-Base64Url ([Security.Cryptography.SHA256]::HashData([Text.Encoding]::ASCII.GetBytes($verifier)))
$state = ConvertTo-Base64Url ([Security.Cryptography.RandomNumberGenerator]::GetBytes(32))
$authorizeUrl = "$($metadata.authorization_endpoint)?response_type=code&client_id=$([uri]::EscapeDataString($clientId))&redirect_uri=$([uri]::EscapeDataString($redirectUri))&scope=$([uri]::EscapeDataString('openid api resources.read offline_access'))&code_challenge=$challenge&code_challenge_method=S256&state=$state"
Write-Host "Open in your browser: $authorizeUrl"
$code = Read-Host 'Returned code'
$returnedState = Read-Host 'Returned state'
if ($returnedState -cne $state) { throw 'State mismatch' }
$basic = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("${clientId}:${clientSecret}"))
$headers = @{ Authorization = "Basic $basic" }
$tokens = Invoke-RestMethod -Method Post -Uri $metadata.token_endpoint -Headers $headers `
-ContentType 'application/x-www-form-urlencoded' -Body @{
grant_type = 'authorization_code'; code = $code
redirect_uri = $redirectUri; code_verifier = $verifier
}
$accessToken = $tokens.access_token
$refreshToken = $tokens.refresh_token
# The chosen API may require additional headers beyond this bearer-token example.
Invoke-RestMethod -Uri $apiUrl -Headers @{ Authorization = "Bearer $accessToken" }
$updated = Invoke-RestMethod -Method Post -Uri $metadata.token_endpoint -Headers $headers `
-ContentType 'application/x-www-form-urlencoded' -Body @{
grant_type = 'refresh_token'; refresh_token = $refreshToken
}
$accessToken = $updated.access_token
if ($updated.refresh_token) { $refreshToken = $updated.refresh_token }
$expiresAt = [DateTimeOffset]::UtcNow.AddSeconds($updated.expires_in)
Write-Host "Refreshed access token expires (UTC): $expiresAt"
# Persist the refreshed tokens securely; never log their values.
// .NET 10 console example: BCL only; browser login is performed interactively.
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
var serviceUrl = "YOUR_ISSUED_AUTHORIZATION_SERVICE_URL";
var clientId = "YOUR_CLIENT_ID";
var clientSecret = Environment.GetEnvironmentVariable("AIVA_CLIENT_SECRET")!;
var apiUrl = "YOUR_APPROVED_API_ENDPOINT";
var redirectUri = "https://integration.example.com/oauth/callback";
using var http = new HttpClient();
using var discovery = JsonDocument.Parse(await http.GetStringAsync($"{serviceUrl.TrimEnd('/')}/.well-known/openid-configuration"));
var authorizationEndpoint = discovery.RootElement.GetProperty("authorization_endpoint").GetString()!;
var tokenEndpoint = discovery.RootElement.GetProperty("token_endpoint").GetString()!;
static string Base64Url(byte[] value) => Convert.ToBase64String(value).TrimEnd('=').Replace('+', '-').Replace('/', '_');
var verifier = Base64Url(RandomNumberGenerator.GetBytes(32));
var challenge = Base64Url(SHA256.HashData(Encoding.ASCII.GetBytes(verifier)));
var state = Base64Url(RandomNumberGenerator.GetBytes(32));
var query = new Dictionary<string, string>
{
["response_type"] = "code", ["client_id"] = clientId, ["redirect_uri"] = redirectUri,
["scope"] = "openid api resources.read offline_access", ["code_challenge"] = challenge,
["code_challenge_method"] = "S256", ["state"] = state
};
var authorizeUrl = $"{authorizationEndpoint}?{string.Join("&", query.Select(p => $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}"))}";
Console.WriteLine($"Open in your browser: {authorizeUrl}");
Console.Write("Returned code: ");
var code = Console.ReadLine()!;
Console.Write("Returned state: ");
if (Console.ReadLine() != state) throw new InvalidOperationException("State mismatch");
async Task<JsonDocument> RequestTokensAsync(Dictionary<string, string> form)
{
using var request = new HttpRequestMessage(HttpMethod.Post, tokenEndpoint) { Content = new FormUrlEncodedContent(form) };
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{clientId}:{clientSecret}")));
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
return JsonDocument.Parse(await response.Content.ReadAsStringAsync());
}
using var tokens = await RequestTokensAsync(new()
{
["grant_type"] = "authorization_code", ["code"] = code,
["redirect_uri"] = redirectUri, ["code_verifier"] = verifier
});
var accessToken = tokens.RootElement.GetProperty("access_token").GetString()!;
var refreshToken = tokens.RootElement.GetProperty("refresh_token").GetString()!;
using var apiRequest = new HttpRequestMessage(HttpMethod.Get, apiUrl);
apiRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
using var apiResponse = await http.SendAsync(apiRequest);
apiResponse.EnsureSuccessStatusCode(); // Follow your API's own authentication contract.
using var updated = await RequestTokensAsync(new()
{
["grant_type"] = "refresh_token", ["refresh_token"] = refreshToken
});
accessToken = updated.RootElement.GetProperty("access_token").GetString()!;
if (updated.RootElement.TryGetProperty("refresh_token", out var replacement)) refreshToken = replacement.GetString()!;
var expiresAt = DateTimeOffset.UtcNow.AddSeconds(updated.RootElement.GetProperty("expires_in").GetInt32());
Console.WriteLine($"Refreshed access token expires (UTC): {expiresAt:u}");
// In a real web app, persist state/verifier and tokens in secure server-side storage.
Scopes & Permissions
Scopes define the level of access an application can request when interacting with AIVA, limiting which resources and operations the application is authorised to access at the API level.
User Permissions determine what an authenticated user is allowed to access or perform within AIVA, ensuring that applications can only act within the permissions already granted to that user.
As of this version only delegated scopes can be generated. Delegated scopes are attached to a user.
Application scopes are not currently supported.
The table below documents the available scopes that can be registered against a client and added to the authorize request.
| Scope | Purpose | Notes |
|---|---|---|
api |
Required base access to the AIVA API. | Added to approved clients; request it during authorization. |
resources.read |
Read permitted AIVA resource data. | Requires AIVA user permission to the target resource. |
resources.readwrite |
Read/create supported AIVA resources. | Does not grant unrestricted write access. |
transfers.readwrite |
Manage supported package-transfer transactions. | Resource/user authorization still applies. |
offline_access |
Requests refresh-token support. | Standard OAuth/OIDC scope, not a resource permission. |
openid |
Requests OpenID Connect identity semantics. | Not an AIVA API access permission. |
Ask for only the scopes your integration needs. AIVA administrators approve capabilities per client. An AIVA user then authenticates through the AIVA-hosted experience; access remains subject to that user’s existing resource permissions. Your integration must be approved before requesting these scopes.
Troubleshooting
Authorization endpoint errors may appear as redirects to the registered callback; token endpoint errors are
returned in JSON. Handle errors by their standard OAuth error identifier, not by comparing
human-readable descriptions.
| Error / Symptom | Likely Cause | Recommendation |
|---|---|---|
invalid_client |
Bad client ID/secret or missing confidential client authentication. | Verify the correct environment's credentials; regenerate the secret if necessary. |
invalid_grant |
Code expired or reused, verifier mismatch, or an invalid/expired grant. | Stop replaying the grant; initiate a fresh authorization when appropriate. |
invalid_scope / unauthorized_client |
Unapproved scope or unsupported flow. | Check assigned client scopes; use Authorization Code + S256 PKCE. |
invalid_request |
Missing/invalid parameters, callback or PKCE values. | Compare with discovery and the registered redirect URI. |
login_required |
prompt=none was used but an interactive login is necessary. |
Restart in an interactive browser, without prompt=none. |
| Browser loops back to sign-in | Browser cookies blocked or stale state; forced login parameters re-applied by the integration. | Inspect callback handling, session cookies and redirects without exposing tokens. |
| HTTP 429 | Hosted authentication or protocol rate limit. | Avoid rapid retries and repeated OTP sends, respect the Retry-After response header.
|
| API 401/403 with a valid JWT | Expired or unsuitable token, missing required scope, or insufficient user permission. | Check token expiry, granted scopes and the API integration instructions; confirm the AIVA user can access the resource. |
{
"error": "invalid_grant",
"error_description": "The token is no longer valid."
}
Never retry a consumed authorization code indefinitely. Start a new interactive authorization flow if the delegated session cannot be renewed.
Session Controls
Session Controls allow applications to influence how AIVA handles existing authenticated sessions, including whether users can continue without signing in again or must reauthenticate. These controls are optional parameters supplied as part of the OAuth authorization request.
| Parameter | Behaviour |
|---|---|
prompt=none |
Attempts authorization without user interaction. Returns login_required
if an existing authenticated session cannot be used.
|
prompt=login |
Forces the user to authenticate again, even when an existing session is available. |
max_age=300 |
Specifies the maximum acceptable age of the user's authentication, in seconds.
For example, 300 requires reauthentication if the user last signed
in more than five minutes ago. Use max_age=0 to require fresh authentication.
|
prompt=select_account |
Requests a new sign-in, allowing the user to authenticate with a different account. AIVA does not display a separate account selection screen. |
prompt=consent |
Not supported. Do not include this value in authorization requests. |
Applying Session Controls
Session Controls are supplied as query-string parameters on the authorization endpoint
obtained from the OpenID Connect discovery document. Include them alongside the standard
authorization parameters, such as client_id, redirect_uri,
scope and PKCE values.
For example, append either of the following parameters to an existing authorization request:
&prompt=login
Or, to require authentication within the last five minutes:
&max_age=300
If using an OAuth or OpenID Connect client library, these values can typically be supplied through its additional authorization request parameters.
Handling Non-Interactive Authentication
When using prompt=none, applications must be prepared to handle a
login_required response. This indicates that interactive authentication
is necessary. The application should initiate a normal authorization request without
prompt=none, allowing the user to complete the AIVA login process.
Session Controls affect authentication during the authorization flow. They do not change access-token lifetimes, refresh-token behaviour or the application's own sign-out process.
Integration Security Checklist
- Keep all client secrets and refresh tokens on a trusted backend. Use TLS for every OAuth call.
- Register exact HTTPS callback URIs. Never redirect to an unvalidated user-provided destination.
- Use a fresh cryptographic PKCE verifier and S256 challenge for every authorization request.
- Generate unpredictable
state, bind it to the initiating session and validate it on callback. - Redeem codes once, server-side.
- Do not log query strings containing codes, password-reset links, secrets or tokens.
- Use a maintained OAuth/OIDC client library; never treat a decoded access token as proof of permission to a specific AIVA resource.
- Protect refresh tokens at rest and prevent concurrent refreshes from racing over stored credentials.
- Handle authentication failures, account changes, 429 responses and missing scope/resource access without retry storms.
- Never ask the end user to send an AIVA password or emailed OTP to your integration.
Glossary of Terms
The following terms are used throughout this guide and in OAuth and OpenID Connect integration examples.
| Term | Meaning |
|---|---|
| Access Token | A credential issued following successful authorization that an application presents when accessing a protected API on behalf of the authenticated user. |
Audience (aud) |
Identifies the intended recipient of a token. An API should only accept access tokens issued for its expected audience. |
| Authentication | The process of verifying who a user is. AIVA handles user authentication through its hosted sign-in experience. |
| Authorization | The process of determining what an authenticated user or application is permitted to do. |
| Authorization Code | A short-lived, single-use value returned to the application after a successful authorization request. The application exchanges it for tokens. |
| Authorization Code Flow | An OAuth 2.0 flow that redirects the user to AIVA for sign-in and returns an authorization code that the application exchanges for tokens without handling the user’s credentials. |
| Authorization Endpoint | The URL to which an application redirects the user to begin authorization. Obtain it from the environment’s discovery document. |
| Authorization Server | The AIVA service that authenticates users, processes OAuth/OIDC authorization requests and issues tokens. |
| Client / Client Application | The software integration requesting authorization to access AIVA on behalf of a user. |
Client ID (client_id) |
The identifier assigned to an approved integration during registration. |
| Client Secret | A confidential credential used to authenticate the registered client when exchanging or refreshing tokens. It must remain on a trusted server. |
Code Challenge (code_challenge) |
A value derived from the PKCE code verifier and included in the authorization request. |
Code Verifier (code_verifier) |
A cryptographically random value retained by the client and sent during the code exchange to prove it initiated the authorization request. |
| Confidential Client | An application able to protect its client secret, typically by performing token requests on a trusted backend rather than in browser code. |
| Delegated Access | Access exercised by an application on behalf of a signed-in AIVA user, subject to both the application’s scopes and that user’s AIVA permissions. |
| Discovery Document | OpenID Connect metadata that publishes the environment’s issuer, authorization endpoint, token endpoint and signing-key location. |
| Grant Type | The OAuth mechanism used to request tokens, such as authorization_code or refresh_token. |
Issuer (iss) |
Identifies the Authorization Server that issued a token. Applications should validate it against the expected environment. |
| JSON Web Token (JWT) | A structured token format containing claims such as subject, issuer, audience and expiry, protected by a digital signature. |
| JSON Web Key Set (JWKS) | The public signing keys published by the Authorization Server so applications and APIs can validate signed tokens. |
| OpenID Connect (OIDC) | An identity layer built on OAuth 2.0 that allows applications to authenticate users and receive standard identity information. |
| OAuth 2.0 | A standard authorization framework that allows an application to request limited access to protected resources using tokens. |
| One-Time Token (OTP) | A temporary verification code that AIVA may ask the user to enter as part of hosted sign-in. |
| PKCE (Proof Key for Code Exchange) | An extension to the Authorization Code flow that prevents an intercepted authorization code being redeemed without the original code verifier. |
Redirect URI (redirect_uri) |
An approved callback address to which the Authorization Server returns the user after the authorization step. It must match a URI registered for the client. |
| Refresh Token | A credential that allows a confidential client to request a replacement access token without requiring another interactive sign-in while renewal remains permitted. |
| Resource Server | An API that accepts access tokens and independently checks whether the request can access a protected resource. |
| S256 | The PKCE method that derives the code challenge using SHA-256 followed by Base64URL encoding. AIVA requires this method. |
Scope (scope) |
A named permission requested by a client, describing the category of API capability the integration is approved to use. |
Subject (sub) |
The claim identifying the authenticated AIVA user represented by a delegated token. |
| Token Endpoint | The URL used by the confidential client to exchange authorization codes or refresh tokens for new tokens. Obtain it from discovery. |
| User Permissions | The permissions assigned to the signed-in user within AIVA, which continue to restrict which resources the integration may access. |
Reference
- AIVA Documentation Hub for Blueprint and
.aivapkgpackage guidance - RFC 7636 — PKCE
- RFC 6749 — OAuth 2.0
- OpenID Connect Core