OAuth 2.0 Authentication
What is OAuth 2.0?
OAuth 2.0 is an industry-standard way for an application to prove who it is without sending a long-lived password on every request. Instead of your gateway application name and password, your application uses an OAuth client: a client ID and client secret that it exchanges for a short-lived access token.
We use the OAuth 2.0 Client Credentials grant (RFC 6749 section 4.4), which is designed for server-to-server integrations where no end user is present.
Benefits
- Short-lived credentials on the wire. Access tokens expire after a few minutes, so a leaked token is only useful for a short time.
- Least privilege. Each OAuth client is granted only the permissions (scopes) it needs, for example send-only.
- Independent credentials per system. Create a separate OAuth client for each system or environment, and deactivate one without affecting the others.
Which APIs support OAuth 2.0?
Each API’s documentation says whether it supports OAuth 2.0 in its Authentication section, along with the scopes it uses and the errors it returns. Where an API supports OAuth 2.0, your existing authentication continues to work, so you can move to OAuth 2.0 at your own pace.
How it works
- Create an OAuth client for one of your gateway applications and choose the permissions (scopes) it needs. You receive a client ID and a client secret.
- Request an access token. Your application sends the client ID and secret to the token URL and receives an access token.
- Call the API. Your application sends the access token in the
Authorization: Bearerheader on each API request. - Reuse the token until it expires, then request a new one with the same client ID and secret.
An access token acts as the gateway application it belongs to. Messages you send with it use that application’s sender IDs, routing, callbacks, IP allowlist and billing, exactly as they do with your application name and password.
Key concepts
| Term | Description |
|---|---|
| OAuth client | A set of credentials linked to one of your gateway applications. An application can have several OAuth clients. |
| Client ID | The public identifier of an OAuth client: a 32-character string of lowercase letters and digits. |
| Client secret | The private password for an OAuth client. It is shown once, when the client is created. |
| Access token | A short-lived token returned by the token URL. Send it as Authorization: Bearer <access_token>. |
| Scope | A permission granted to an OAuth client, written as <api>/<permission>, for example sms.restv2/message.send. Each API lists its scopes in its Authentication section. |
| Token URL | The endpoint that issues access tokens: https://api.sms.optus.com.au/oauth2/token |
An OAuth client only works with the API its application belongs to. A token issued for one API is rejected by the others.
Getting OAuth credentials
You create OAuth clients for an application in sms.optus.com.au, on that application’s API Configuration page. Each API’s Authentication section has the exact steps.
When you create an OAuth client:
- Give it a name that identifies where it is used, for example
Billing system - production. - Select only the permissions that system needs.
- Copy the Client ID and Client Secret and store them securely, for example in your secrets manager.
Requesting an access token
Send a POST request to the token URL with an
application/x-www-form-urlencoded body. Send the client ID and secret in the
request body. HTTP Basic authentication isn’t supported on the token URL.
POST https://api.sms.optus.com.au/oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=<client_id>&client_secret=<client_secret>&scope=<scope>
| Parameter | Required | Description |
|---|---|---|
grant_type |
Yes | Must be client_credentials. |
client_id |
Yes | Your OAuth client ID. |
client_secret |
Yes | Your OAuth client secret. |
scope |
No | A space-separated list of scopes. Leave it out to receive every scope granted to the client. Every scope you request must have been granted to the client. |
On success:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"access_token": "eyJhbGciOiJFZERTQSIsImtpZCI6Ii4uLiIsInR5cCI6ImF0K2p3dCJ9.eyJ...",
"token_type": "Bearer",
"expires_in": 900,
"scope": "sms.restv2/message.send"
}
| Field | Description |
|---|---|
access_token |
The token to send on API requests. Treat it as opaque, and don’t rely on its contents. |
token_type |
Always Bearer. |
expires_in |
The number of seconds until the token expires. |
scope |
The scopes granted to this token. |
No refresh token is issued. When a token is about to expire, request a new one with your client credentials.
Token URL errors
Errors from the token URL use the standard OAuth 2.0 error format:
{
"error": "invalid_client",
"error_description": "Authentication failed"
}
| HTTP status | error |
Meaning and what to do |
|---|---|---|
| 400 | invalid_request |
A required parameter is missing or badly formatted. Check that the body is form-encoded and includes client_id and client_secret. |
| 400 | unsupported_grant_type |
grant_type must be client_credentials. |
| 400 | invalid_scope |
You requested a scope that hasn’t been granted to this client, or the scope is badly formatted. Remove scope to receive every granted scope. |
| 401 | invalid_client |
The client ID or secret is wrong, or the client is deactivated or revoked. |
| 429 | too_many_requests |
Too many token requests for this client. Reuse tokens until they expire, and wait before retrying. |
| 500 | server_error |
Something went wrong on our side. Retry with a backoff. If it continues, contact support. |
For security reasons, invalid_client doesn’t say which part of the
credentials was wrong.
Example: sending an SMS with REST v2
This example shows the whole flow with REST v2, using an OAuth
client that has the sms.restv2/message.send scope.
1. Request an access token
curl -X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'client_secret=YOUR_CLIENT_SECRET' \
--data-urlencode 'scope=sms.restv2/message.send' \
https://api.sms.optus.com.au/oauth2/token
const response = await fetch("https://api.sms.optus.com.au/oauth2/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
scope: "sms.restv2/message.send"
})
});
const token = await response.json();
// token.access_token, token.expires_in
import os
import requests
response = requests.post(
"https://api.sms.optus.com.au/oauth2/token",
data={
"grant_type": "client_credentials",
"client_id": os.environ["CLIENT_ID"],
"client_secret": os.environ["CLIENT_SECRET"],
"scope": "sms.restv2/message.send",
},
timeout=10,
)
response.raise_for_status()
token = response.json()
print(token["expires_in"])using System.Net.Http.Json;
using var client = new HttpClient();
var form = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = Environment.GetEnvironmentVariable("CLIENT_ID")!,
["client_secret"] = Environment.GetEnvironmentVariable("CLIENT_SECRET")!,
["scope"] = "sms.restv2/message.send"
});
var response = await client.PostAsync("https://api.sms.optus.com.au/oauth2/token", form);
response.EnsureSuccessStatusCode();
var token = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();2. Send an SMS with the access token
Send the access token in the Authorization header. Everything else about the
request is the same as with your application name and password.
curl -X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"destination": "+61491570156", "content": "Hello world!"}' \
https://api.sms.optus.com.au/rest/sms/v2/messages
const response = await fetch("https://api.sms.optus.com.au/rest/sms/v2/messages", {
method: "POST",
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
destination: "+61491570156",
content: "Hello world!"
})
});
const result = await response.json();import requests
access_token = "ACCESS_TOKEN_FROM_STEP_1"
response = requests.post(
"https://api.sms.optus.com.au/rest/sms/v2/messages",
headers={
"Accept": "application/json",
"Authorization": f"Bearer {access_token}",
},
json={"destination": "+61491570156", "content": "Hello world!"},
timeout=10,
)
if response.status_code == 202:
print(response.json())
else:
print("Error:", response.status_code, response.text)using System.Net.Http.Headers;
using System.Text;
using var client = new HttpClient();
var request = new HttpRequestMessage(
HttpMethod.Post,
"https://api.sms.optus.com.au/rest/sms/v2/messages");
request.Headers.Accept.ParseAdd("application/json");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
request.Content = new StringContent(
"""{"destination":"+61491570156","content":"Hello world!"}""",
Encoding.UTF8,
"application/json");
var response = await client.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();On success, REST v2 returns 202 Accepted with the message UUID, as described
in Sending messages.
Reuse tokens until they expire
Request a token once and reuse it for every API call until it is close to expiry. Don’t request a new token for every message. The token URL is rate limited per client, and requesting a token on each message adds latency to every send.
A reliable pattern is:
- Cache the access token together with the time it expires
(
now + expires_in). - Before each API call, reuse the cached token if it has at least 60 seconds left. Otherwise, request a new one.
- If an API call rejects the token, request a new token once and retry the call. If it fails again, stop and investigate. Don’t retry in a loop. Each API’s Authentication section lists the errors it returns.
Managing OAuth clients
You can see your OAuth clients and change their status on the application’s API Configuration page.
| Status | Can request new tokens | Can change to |
|---|---|---|
| Active | Yes | Deactivated, Revoked |
| Deactivated | No | Active, Revoked |
| Revoked | No. This is permanent. | None |
- Deactivate a client to pause it temporarily, for example while you investigate a suspected leak. You can activate it again later with the same client ID and secret.
- Revoke a client when it is no longer needed or its secret may have been exposed. Revoking can’t be undone.
Replacing a client secret
To replace a secret without downtime:
- Create a new OAuth client with the same permissions.
- Update your systems to use the new client ID and secret.
- Confirm your systems are working with the new credentials.
- Revoke the old OAuth client.
Security recommendations
- Store client secrets in a secrets manager or encrypted configuration. Never put them in source code, logs or client-side (browser or mobile) apps.
- Create a separate OAuth client for each system and each environment, and give each one only the scopes it needs.
- Treat access tokens as secrets. Don’t log them.
- Always use HTTPS and verify our TLS certificate. See SSL Certificates.
- Review your OAuth clients regularly, and revoke any that are no longer in use.
- Once all your systems use OAuth 2.0, ask optussd@modicagroup.com about retiring the password for that application, so only OAuth credentials can be used.