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

  1. 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.
  2. Request an access token. Your application sends the client ID and secret to the token URL and receives an access token.
  3. Call the API. Your application sends the access token in the Authorization: Bearer header on each API request.
  4. 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.
warningThe client secret is shown only once. It can’t be retrieved later. If you lose it, create a new OAuth client and revoke the old one.

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:

  1. Cache the access token together with the time it expires (now + expires_in).
  2. Before each API call, reuse the cached token if it has at least 60 seconds left. Otherwise, request a new one.
  3. 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:

  1. Create a new OAuth client with the same permissions.
  2. Update your systems to use the new client ID and secret.
  3. Confirm your systems are working with the new credentials.
  4. 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.