diff --git a/src/data/nav/platform.ts b/src/data/nav/platform.ts index 99e86ae10a..a853c2b9ca 100644 --- a/src/data/nav/platform.ts +++ b/src/data/nav/platform.ts @@ -18,6 +18,28 @@ export default { }, ], }, + { + name: 'Authentication', + pages: [ + { + name: 'Overview', + link: '/docs/platform/auth', + index: true, + }, + { + name: 'Authentication methods', + link: '/docs/platform/auth/authentication', + }, + { + name: 'Identified clients & capabilities', + link: '/docs/platform/auth/capabilities', + }, + { + name: 'Token Revocation', + link: '/docs/platform/auth/revocation', + }, + ], + }, { name: 'Architecture', pages: [ diff --git a/src/data/nav/pubsub.ts b/src/data/nav/pubsub.ts index 7c66eea192..f581f9ea4c 100644 --- a/src/data/nav/pubsub.ts +++ b/src/data/nav/pubsub.ts @@ -101,28 +101,20 @@ export default { pages: [ { name: 'Overview', - link: '/docs/auth', + link: '/docs/platform/auth', index: true, }, { - name: 'Basic auth', - link: '/docs/auth/basic', + name: 'Authentication Methods', + link: '/docs/platform/auth/authentication', }, { - name: 'Token auth', - link: '/docs/auth/token', + name: 'Permissions & Capabilities', + link: '/docs/platform/auth/capabilities', }, { - name: 'Token revocation', - link: '/docs/auth/revocation', - }, - { - name: 'Identified clients', - link: '/docs/auth/identified-clients', - }, - { - name: 'Capabilities', - link: '/docs/auth/capabilities', + name: 'Token Revocation', + link: '/docs/platform/auth/revocation', }, ], }, diff --git a/src/pages/docs/auth/capabilities.mdx b/src/pages/docs/auth/capabilities.mdx index fd25089dc0..21720d4300 100644 --- a/src/pages/docs/auth/capabilities.mdx +++ b/src/pages/docs/auth/capabilities.mdx @@ -7,7 +7,7 @@ API keys and Ably-compatible tokens, have a set of capabilities assigned to them API keys are long-lived, secret and typically not shared with clients. API key capabilities are configured using the [dashboard](https://ably.com/dashboard), or using the [Control API](/docs/platform/account/control-api). -Ably-compatible tokens are designed to be shared with untrusted clients, are short-lived, and can be configured and issued programmatically. See [selecting an authentication mechanism](/docs/auth#selecting-auth) to understand why token authentication is the preferred option in most scenarios. +Ably-compatible tokens are designed to be shared with untrusted clients, are short-lived, and can be configured and issued programmatically. See [selecting an authentication mechanism](/docs/platform/auth/authentication#how-to-choose-authentication-method) to understand why token authentication is the preferred option in most scenarios. ## Resource names and wildcards @@ -143,7 +143,7 @@ The capabilities for tokens are determined based on those of the issuing API key #### Ably Token without capabilities -If no capability is specified in an Ably `TokenRequest`, then the [Ably Token](/docs/auth/token#tokens) will be given the full set of capabilities assigned to the issuing key. +If no capability is specified in an Ably `TokenRequest`, then the [Ably Token](/docs/platform/auth/authentication#ably-tokens) will be given the full set of capabilities assigned to the issuing key. Using the following example, an API key exists with the listed capabilities. If an Ably Token is requested without specifying any capabilities then the `TokenRequest` is treated as requesting all capabilities, i.e. `{"[*]*":["*"]}`. This will result in the Ably Token receiving all the capabilities of the API key. @@ -257,7 +257,7 @@ final tokenRequest = await realtime.auth.requestToken(tokenParams: tokenParams); If a set of capabilities are requested, then the Ably Token will be assigned the intersection of the requested capability and the capability of the issuing key. -Using the following example, an API key exists with the listed capabilities. If an [Ably Token](/docs/auth/token#tokens) is requested and specifies a set of capabilities, then the resulting token will only receive those capabilities that intersect. The capabilities of a token cannot exceed those of the issuing API key. +Using the following example, an API key exists with the listed capabilities. If an [Ably Token](/docs/platform/auth/authentication#ably-tokens) is requested and specifies a set of capabilities, then the resulting token will only receive those capabilities that intersect. The capabilities of a token cannot exceed those of the issuing API key. ```javascript @@ -413,7 +413,7 @@ final tokenDetails = await rest.auth.requestToken(tokenParams: tokenParams); If a set of capabilities are requested, and the intersection between those and the API key's capabilities is empty, then the `TokenRequest` will result in an error. -Using the following example, an API key exists with the listed capabilities. If an [Ably Token](/docs/auth/token#tokens) is requested that specifies a set of capabilities, and there is no intersection between the capabilities of the issuing API key and requested token, then the token request will be rejected. In the following example, the callback will be returned with an error. +Using the following example, an API key exists with the listed capabilities. If an [Ably Token](/docs/platform/auth/authentication#ably-tokens) is requested that specifies a set of capabilities, and there is no intersection between the capabilities of the issuing API key and requested token, then the token request will be rejected. In the following example, the callback will be returned with an error. ```javascript @@ -507,7 +507,7 @@ final tokenDetails = await realtime.auth.requestToken(tokenParams: tokenParams); #### Ably JWT capability determination -Capabilities are determined for [Ably JWTs](/docs/auth/token#jwt) in the following way: +Capabilities are determined for [Ably JWTs](/docs/platform/auth/authentication#json-web-tokens-jwt) in the following way: * The capabilities granted to an Ably JWT will be the intersection of the capabilities within the Ably JWT and the capabilities of the associated API key. * If the set of capabilities within the Ably JWT have no intersection with the capabilities of the API key, then an error will instead be returned. diff --git a/src/pages/docs/platform/auth/authentication.mdx b/src/pages/docs/platform/auth/authentication.mdx new file mode 100644 index 0000000000..db702e8bf0 --- /dev/null +++ b/src/pages/docs/platform/auth/authentication.mdx @@ -0,0 +1,1062 @@ +--- +title: Authentication Methods +meta_description: "Comprehensive guide to Ably's authentication methods including key authentication, token authentication, JWT, and implementation patterns." +redirect_from: + - /docs/auth/basic + - /docs/auth/token +--- + +Ably provides multiple authentication methods designed for different use cases and security requirements. This page covers the implementation details for each method and when to use them. + +## How to choose authentication method? + +When deciding on which authentication method you will be using, it is recommended you bear in mind the [principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege). + +A client should ideally only possess the credentials and rights that it needs to accomplish what it wants. This way, if the credentials are compromised, the rights that can be abused by an attacker are minimized. + +The following table provides guidelines on what to consider when choosing your authentication method. Many applications will most naturally use a mixed strategy: one or more trusted application servers will use basic authentication to access the service and issue tokens over HTTPS, whereas remote browsers and devices will use individually issued tokens: + +| Scenario | [Key](#key-authentication) | [Token](#token-based-authentication) | Description | +|----------|---------------------------|---------------------------|-------------| +| Your scripts may be exposed | No | Yes | If the script, program or system holding the key is exposed, for example on a user's device, you should not embed an API key and instead use token authentication. If the script is on a secure environment such as your own server, an API key with basic authentication is fine. | +| Your connection may be insecure | No | Yes | If there is a risk of exposure of the client's credentials, either directly or over an insecure, or insecurely proxied, connection, token authentication should be used. If you are sure the connection is secure and unmediated, basic authentication is acceptable. | +| You have no server to control access | Yes | No | If you do not have your own server to perform authentication and provide tokens to users, you'll need to use basic authentication. | +| You require fine-grained access control | No | Yes | If you need to provide [privileges](/docs/platform/auth/capabilities#capabilities) on a user-by-user basis, you'd be better using token authentication. If you only need a few access control groups, basic authentication is reasonable. | +| Users need restricted periods of access | No | Yes | If you need users to only have access for a certain period of time, or the ability to revoke access, token authentication is needed. If users are able to always have access, basic authentication is acceptable. | +| Users need to identify themselves | Partial | Yes | If the user can be trusted to [identify](/docs/platform/auth/capabilities#identified-clients) itself, basic authentication is fine. If the user cannot be trusted however, token authentication is better as it allows for a trusted token distributor to identify the user instead. | + +## Key authentication + +Key authentication is designed for server-side applications where the API key can be securely stored and not accessed by external clients. + +### Ably API keys + +Every Ably app can have one or more API keys associated with it in order to authenticate directly with Ably, or to issue tokens with. API keys can be created with different capabilities and any tokens issued using that API key can only permit a subset of those capabilities. + +#### API key format + +An Ably API key string has the following format: `I2E_JQ.OqUdfg:EVKVTCBlzLBPYJiCZTsIW_pqylJ9WVRB5K9P19Ap1y0`. + +The API key is made up of three parts: + +1. `I2E_JQ` is the public **app ID** (the part before the first period) +2. `OqUdfg` is the public app **key ID** (the part after the period and before the colon). `I2E_JQ.OqUdfg` is the public API key ID (both the public app ID and app key ID together) +3. `EVKVTCBlzLBPYJiCZTsIW_pqylJ9WVRB5K9P19Ap1y0` is the API **key secret** and should never be shared with untrusted parties (the part after the colon) + +The API key secret is private and should never be made public. This API key string is used in all Ably SDKs and for authentication with the REST API. + +### How to create an API key + +API keys are created in the [Ably dashboard](https://ably.com/dashboard). You can also create an API key programmatically using the [Control API](/docs/platform/account/control-api). + +To create an API key in the Ably dashboard: + +1. In your [Ably dashboard](https://ably.com/dashboard) click the API Keys tab. +2. Click the **Create a new API key** button. +3. Enter a name for your API key - this will help you identify the specific key when you have many keys created. +4. Select the [capabilities](/docs/auth/capabilities) you want to apply to the key. Clients connecting with this key will be restricted to these capabilities. +5. Optionally you can select whether to make tokens generated from this key to be revocable or not. +6. Optionally select whether you want to restrict the scope of the key to channels, queues, or specific channels and queues using resource names and wildcards. + + + +#### Authentication with a key + +The following is an example of using key authentication: + + +```realtime_javascript +const realtime = new Ably.Realtime({ + key: '{{API_KEY}}' +}); +``` + +```realtime_nodejs +const realtime = new Ably.Realtime({ + key: '{{API_KEY}}' +}); +``` + +```realtime_ruby +realtime = Ably::Realtime.new(key: '{{API_KEY}}') +``` + +```realtime_python +realtime = AblyRealtime(key='{{API_KEY}}') +``` + +```realtime_java +ClientOptions options = new ClientOptions(); +options.key = "{{API_KEY}}"; +AblyRealtime realtime = new AblyRealtime(options); +``` + +```realtime_swift +let realtime = ARTRealtime(key: "{{API_KEY}}") +``` + +```realtime_objc +ARTRealtime *realtime = [[ARTRealtime alloc] initWithKey:@"{{API_KEY}}"]; +``` + +```realtime_csharp +AblyRealtime realtime = AblyRealtime("{{API_KEY}}"); +``` + +```realtime_go +client, err := ably.NewRealtime(ably.WithKey("{{API_KEY}}")) +``` + +```realtime_flutter +final clientOptions = ably.ClientOptions( + key: '{{API_KEY}}' +); +final realtime = ably.Realtime(options: clientOptions); +``` + +```rest_javascript +var rest = new Ably.Rest({ key: '{{API_KEY}}' }); +``` + +```rest_nodejs +var rest = new Ably.Rest({ key: '{{API_KEY}}' }); +``` + +```rest_ruby +rest = Ably::Rest.new(key: '{{API_KEY}}') +``` + +```rest_python +rest = AblyRest(key='{{API_KEY}}') +``` + +```rest_java +ClientOptions options = new ClientOptions(); +options.key = "{{API_KEY}}"; +AblyRest rest = new AblyRest(options); +``` + +```rest_swift +let rest = ARTRest(key: "{{API_KEY}}") +``` + +```rest_objc +ARTRest *rest = [[ARTRest alloc] initWithKey:@"{{API_KEY}}"]; +``` + +```rest_csharp +AblyRest rest = new AblyRest("{{API_KEY}}"); +``` + +```rest_go +client, err := ably.NewREST(ably.WithKey("{{API_KEY}}")) +``` + +```rest_flutter +final clientOptions = ably.ClientOptions( + key: '{{API_KEY}}' +); +ably.Rest rest = ably.Rest(options: clientOptions); +``` + +```rest_php +$rest = new Ably\AblyRest(['key' => '{{API_KEY}}']); +``` + + +## Token-based authentication + +Token authentication uses a trusted device with an [API key](#ably-api-keys) to issue time-limited tokens to untrusted clients. Tokens have a limited set of access rights, known as [capabilities](/docs/platform/auth/capabilities#capabilities), and can have a specific [identity](/docs/platform/auth/capabilities#identified-clients) using a `clientId`. + +Token authentication is the recommended authentication method to use client-side as it provides more fine-grained access control and limits the risk of credentials being exposed. + +Any of the following cause an SDK to use token authentication: + +* An [`authUrl`](/docs/api/realtime-sdk/types#client-options) or [`authCallback`](/docs/api/realtime-sdk/types#client-options) is provided that returns an Ably-compatible token or an Ably [`TokenRequest`](/docs/api/realtime-sdk/types#token-request) +* [`useTokenAuth`](/docs/api/realtime-sdk/types#client-options) is true +* A [`token`](/docs/api/realtime-sdk/types#client-options) or [`tokenDetails`](/docs/api/realtime-sdk/types#client-options) property is provided + +Ably supports two types of tokens: + +| Type | Description | +|----------|-------------| +| JWT tokens | JSON Web Tokens (JWT) provide a standardized, secure way to handle authentication with additional flexibility for custom claims. | +| Ably tokens | Ably Tokens are issued directly by the Ably service using your API key. | + +### JSON Web Tokens (JWT) + +#### Creating Ably JWTs + +Ably JWTs must be signed, in your backend, with the following (which can be found in [Ably API keys](#ably-api-keys)): + +- your app ID, +- your API key ID, +- your API key secret + +The JWT must also include specific claims: + + +```javascript +const jwt = require('jsonwebtoken'); + +function createAblyJWT(userId, capabilities) { + const currentTime = Math.floor(Date.now() / 1000); + + const payload = { + iss: 'your-app-id.key-id', // API key name + sub: userId, // Subject (clientId) + iat: currentTime, // Issued at + exp: currentTime + 3600, // Expires in 1 hour + 'x-ably-capability': JSON.stringify(capabilities), + 'x-ably-clientId': userId + }; + + return jwt.sign(payload, 'your-key-secret', { + algorithm: 'HS256', + header: { typ: 'JWT', alg: 'HS256', kid: 'your-app-id.key-id' } + }); +} +``` + +```flutter +final header = { + "typ": "JWT", + "alg": "HS256", + "kid": "{{API_KEY_NAME}}" +}; + +final currentTime = (DateTime.now().millisecondsSinceEpoch / 1000).round(); +final claims = { + "iat": currentTime, /* current time in seconds */ + "exp": currentTime + 3600, /* time of expiration in seconds */ + "x-ably-capability": "{\"*\":[\"*\"]}" +}; + +final base64Header = base64UrlEncode(utf8.encode(json.encode(header))); +final base64Claims = base64UrlEncode(utf8.encode(json.encode(claims))); +final hmacSha256 = Hmac(sha256, utf8.encode("$base64Header.$base64Claims")); +final digest = hmacSha256.convert(utf8.encode("{{API_KEY_SECRET}}")); +final signature = base64UrlEncode(digest.bytes); +final ablyJwt = "$base64Header.$base64Claims.$signature"; +``` + +```java +Map headerClaims = new HashMap<>(); +headerClaims.put("typ", "JWT"); +headerClaims.put("alg", "HS256"); +headerClaims.put("kid", "{{API_KEY_NAME}}"); + +// Define the current time +long currentTimeInSeconds = System.currentTimeMillis() / 1000; + +// Define the claims +Map claims = new HashMap<>(); +claims.put("iat", currentTimeInSeconds); +claims.put("exp", currentTimeInSeconds + 3600); +claims.put("x-ably-capability", "{\"*\":[\"*\"]}"); + +// Create the JWT +Algorithm algorithm = Algorithm.HMAC256("{{API_KEY_SECRET}}"); +String token = JWT.create() + .withHeader(headerClaims) + .withPayload(claims) + .sign(algorithm); +``` + +```php +$header = [ + 'typ' => 'JWT', + 'alg' => 'HS256', + 'kid' => '{{API_KEY_NAME}}' +]; +$currentTime = time(); +$claims = [ + 'iat' => $currentTime, /* current time in seconds */ + 'exp' => $currentTime + 3600, /* time of expiration in seconds (an hour) */ + 'x-ably-capability' => '{\"*\":[\"*\"]}' +]; +$base64Header = base64_encode(json_encode($header)); +$base64Claims = base64_encode(json_encode($claims)); +$signature = hash_hmac( + 'sha256', + $base64Header . '.' . $base64Claims, + '{{API_KEY_SECRET}}', + true +); +$jwt = $base64Header . '.' . $base64Claims . '.' . $signature; +``` + +```python +import jwt +import time + +def create_ably_jwt(user_id, capabilities): + current_time = int(time.time()) + + payload = { + 'iss': 'your-app-id.key-id', # API key name + 'sub': user_id, # Subject (clientId) + 'iat': current_time, # Issued at + 'exp': current_time + 3600, # Expires in 1 hour + 'x-ably-capability': json.dumps(capabilities), + 'x-ably-clientId': user_id + } + + headers = { + 'typ': 'JWT', + 'alg': 'HS256', + 'kid': 'your-app-id.key-id' + } + + return jwt.encode(payload, 'your-key-secret', algorithm='HS256', headers=headers) +``` + +```go +import ( + "time" + "github.com/golang-jwt/jwt/v4" +) + +func createAblyJWT(userID string, capabilities map[string][]string) (string, error) { + now := time.Now() + + claims := jwt.MapClaims{ + "iss": "your-app-id.key-id", // API key name + "sub": userID, // Subject (clientId) + "iat": now.Unix(), // Issued at + "exp": now.Add(time.Hour).Unix(), // Expires in 1 hour + "x-ably-capability": capabilities, + "x-ably-clientId": userID, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + token.Header["kid"] = "your-app-id.key-id" + + return token.SignedString([]byte("your-key-secret")) +} +``` + + +#### Embed an Ably Token in a JWT + +If a system has an existing [JWT](https://jwt.io/) scheme, it's possible to embed an Ably Token as a claim within it. + +The JWT and embedded token need to meet the following requirements: + +* The embedded token is an [Ably Token](#tokens). +* The embedded token is included under the `x-ably-token` key in the [JOSE Header](https://tools.ietf.org/html/rfc7519), or if using JWS, the embedded token is included using the `x-ably-token` claim in the payload. +* The expiry time of the embedded token must not be earlier than the outer JWT's expiry time (`exp` claim). Ably will reject any JWT if it is unencrypted and its `exp` claim is later than the expiry of the enclosed token. This helps to ensure that tokens are renewed prior to expiry. + +The process used by Ably SDKs to authenticate with Ably using an Ably Token embedded in a JWT is illustrated in the following diagram: + +![Token embedded in a JWT auth method](../../../../images/content/diagrams/Ably-Auth-Overview3.png) + +The following is an example of issuing an Ably Token inside the of header of a JWT. Note that the authenticity of the JWT will not be checked, due to Ably not having access to your `SECRET` key. + + +```javascript +const ably = new Ably.Rest({ key: '{{API_KEY}}' }); +const tokenDetails = await ably.auth.requestToken({ clientId: 'client@example.com' }); +const header = { + "typ":"JWT", + "alg":"HS256", + "x-ably-token": tokenDetails.token +} +const claims = { + "exp": currentTime + 3600 +} +const base64Header = btoa(header); +const base64Claims = btoa(claims); +/* Apply the hash specified in the header */ +const signature = hash((base64Header + "." + base64Claims), SECRET); +const jwt = base64Header + "." + base64Claims + "." + signature; +/* Send jwt to client */ +``` + +```python + rest = AblyRest(key='{{API_KEY}}') + token_request_data = { + 'clientId': 'client@example.com', + } + + token_details = await rest.auth.request_token(token_params=token_request_data) + + header = { + "typ": "JWT", + "alg": "HS256", + "x-ably-token": token_details.token + } + claims = { + "exp": int(time.time()) + 3600 + } + + base64_header = base64.urlsafe_b64encode(bytes(json.dumps(header), 'utf-8')).decode('utf-8') + base64_claims = base64.urlsafe_b64encode(bytes(json.dumps(claims), 'utf-8')).decode('utf-8') + + signature = hashlib.sha256((base64_header + "." + base64_claims + "{{API_KEY_SECRET}}").encode('utf-8')).digest() + signature_base64 = base64.urlsafe_b64encode(signature).decode('utf-8') + + jwt_token = base64_header + "." + base64_claims + "." + signature_base64 +``` + +```java +ClientOptions options = new ClientOptions("{{API_KEY}}"); +AblyRest rest = new AblyRest(options); + +Auth.TokenParams tokenParams = new Auth.TokenParams(); +tokenParams.clientId = "client@example.com"; + +Auth.TokenDetails tokenDetails = rest.auth.requestToken(tokenParams, null); + +Map headerClaims = new HashMap<>(); +headerClaims.put("typ", "JWT"); +headerClaims.put("alg", "HS256"); +headerClaims.put("x-ably-token", tokenDetails.token); + +// Time of expiration in seconds (an hour) +long currentTimeInSeconds = System.currentTimeMillis() / 1000; + +// Define the claims +Map claims = new HashMap<>(); +claims.put("exp", currentTimeInSeconds + 3600); + +// Create the JWT +Algorithm algorithm = Algorithm.HMAC256("{{API_KEY_SECRET}}"); +String token = JWT.create() + .withHeader(headerClaims) + .withPayload(claims) + .sign(algorithm); +``` + +```php +$rest = new Ably\AblyRest( + ['key' => '{{API_KEY}}'] +); +$tokenDetails = $rest->auth->requestToken( + ['clientId' => 'client@example.com'] +); +$header = [ + 'typ' => 'JWT', + 'alg' => 'HS256', + 'x-ably-token' => $tokenDetails->token +]; +$currentTime = time(); +$claims = [ + 'exp' => $currentTime + 3600 /* time of expiration in seconds (an hour) */ +]; +$base64Header = base64_encode(json_encode($header)); +$base64Claims = base64_encode(json_encode($claims)); +$secret = 'YOUR_SECRET'; +$signature = hash_hmac('sha256', $base64Header . '.' . $base64Claims, $secret, true); +$base64Signature = base64_encode($signature); +$jwt = $base64Header . '.' . $base64Claims . '.' . $base64Signature; +``` + +```go +rest, err := ably.NewREST(ably.WithKey("{{API_KEY}}")) +if err != nil { + log.Fatalf("Failed to create Ably REST client: %v", err) +} + +// Request a token +tokenParams := &ably.TokenParams{ + ClientID: "client@example.com", +} +tokenDetails, err := rest.Auth.RequestToken(context.Background(), tokenParams) +if err != nil { + log.Fatalf("Failed to request token: %v", err) +} + +// Get the current time in seconds +currentTime := time.Now().Unix() + +// Create JWT header +header := map[string]interface{}{ + "typ": "JWT", + "alg": "HS256", + "x-ably-token": tokenDetails.Token, +} + +// Create JWT claims +claims := map[string]interface{}{ + "exp": currentTime + 3600, // time of expiration in seconds +} + +// Encode header to base64 +headerJSON, err := json.Marshal(header) +if err != nil { + log.Fatalf("Failed to marshal header: %v", err) +} +base64Header := base64.RawURLEncoding.EncodeToString(headerJSON) + +// Encode claims to base64 +claimsJSON, err := json.Marshal(claims) +if err != nil { + log.Fatalf("Failed to marshal claims: %v", err) +} +base64Claims := base64.RawURLEncoding.EncodeToString(claimsJSON) + +// Create the signature +dataToSign := base64Header + "." + base64Claims +h := hmac.New(sha256.New, []byte("SECRET")) +h.Write([]byte(dataToSign)) +signature := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) + +// Combine the parts to form the final JWT +jwt := base64Header + "." + base64Claims + "." + signature +log.Println("JWT:", jwt) + +// Send JWT to client (for demonstration, we print it here) +``` + +```flutter +final clientOptions = ably.ClientOptions( + key: '{{API_KEY}}', +); +final rest = ably.Rest(options: clientOptions); +const tokenParams = ably.TokenParams( + clientId: 'client@example.com', +); +final tokenDetails = await rest.auth.requestToken( + tokenParams: tokenParams +); + +final header = { + "typ": "JWT", + "alg": "HS256", + "x-ably-token": tokenDetails.token +}; + +final currentTime = (DateTime.now().millisecondsSinceEpoch / 1000).round(); +final claims = { + "iat": currentTime, /* current time in seconds */ + "exp": currentTime + 3600, /* time of expiration in seconds */ + "x-ably-capability": "{\"*\":[\"*\"]}" +}; + +final base64Header = base64UrlEncode(utf8.encode(json.encode(header))); +final base64Claims = base64UrlEncode(utf8.encode(json.encode(claims))); +final hmacSha256 = Hmac(sha256, utf8.encode("$base64Header.$base64Claims")); +final digest = hmacSha256.convert(utf8.encode("{{API_KEY_SECRET}}")); +final signature = base64UrlEncode(digest.bytes); +final ablyJwt = "$base64Header.$base64Claims.$signature"; +``` + + +#### Using JWTs with Ably + +There are several possible methods to using a JWT with Ably to authenticate. The preferred method is the `authUrl` method, which along with the callback method have automatic token refresh functionality: + + +```javascript +// With authUrl +const realtime = new Ably.Realtime({ authUrl: '/auth_url_to_your_backend' }); + +// Direct token usage +const ably = new Ably.Realtime({ + token: jwtToken, + clientId: 'user-123' +}); + +// Or via auth callback that generates JWT +const ably = new Ably.Realtime({ + authCallback: async (tokenParams, callback) => { + // generateJWTFromYourServer calls your server to a specific endpoint to retrieve the JWT. + const jwt = await generateJWTFromYourServer(tokenParams); + callback(null, jwt); + } +}); +``` + + +#### Custom JWT claims + +JWTs can include custom claims for application-specific data: + + +```javascript +const payload = { + // Standard Ably claims + iss: 'your-app-id.key-id', + sub: 'user-123', + 'x-ably-capability': '{"*":["*"]}', + 'x-ably-clientId': 'user-123', + + // Custom claims for channel-specific permissions + 'ably.channel.private-user-123': 'owner', + 'ably.channel.public-*': 'participant', + 'ably.channel.*': 'guest', + + // Custom application data + 'userRole': 'premium', + 'features': ['analytics', 'priority-support'] +}; +``` + + +### Ably Tokens + +Ably Tokens can be used to authenticate with Ably in the following ways: + +* Ably [TokenRequest](#token-request) is created by your servers and passed to clients. +* An [Ably Token](#ably-token) is issued by your servers and passed to clients. + +Note that the machine on which you are running your auth server should have an accurate clock, as tokens and `TokenRequest` contain a timestamp. You can use an [NTP daemon](https://en.wikipedia.org/wiki/Ntpd), or if you are not able to control your server's clock, you can wish to use the `queryTime` [auth option](/docs/api/rest-sdk/types#auth-options). + +#### Ably TokenRequest + +Using an Ably SDK, a `TokenRequest` is [generated from your server](/docs/api/realtime-sdk/authentication#create-token-request) and returned to the client-side SDK instance. The client-side SDK instance then uses the [`TokenRequest`](/docs/api/realtime-sdk/types#token-request) to request an [Ably Token](/docs/api/realtime-sdk/authentication#request-token) from Ably, and subsequently authenticates using that [Ably Token](/docs/api/realtime-sdk/authentication#token-details). + +This is the recommended approach for client-side authentication, for the following reasons: + +* An Ably `TokenRequest` can be generated securely by your servers without communicating with Ably. +* Your secret API key is never shared with Ably or your clients. +* An Ably `TokenRequest` cannot be tampered with due to being signed, must be used soon after creation, and can only be used once. + +The process used by Ably SDKs to authenticate with Ably using a `TokenRequest` is illustrated in the following diagram: + +![Ably TokenRequest auth process diagram](../../../../images/content/diagrams/Ably-token-auth-1.png) + +The following is an example of creating an Ably `TokenRequest`, in your backend server to be returned to the client: + + +```javascript +const ably = new Ably.Rest({ key: '{{API_KEY}}' }); +const tokenRequest = await ably.auth.createTokenRequest({ clientId: 'client@example.com' }); +``` + +```python +ably = AblyRest('{{API_KEY}}') +token = await ably.auth.create_token_request( +{ + "clientId": "client@example.com", + "capability": { + "channel1": ["publish", "subscribe"], + }, + 'ttl': 3600 * 1000, # ms +}) +``` + +```java +ClientOptions options = new ClientOptions("{{API_KEY}}"); +AblyRest rest = new AblyRest(options); + +Auth.TokenParams tokenParams = new Auth.TokenParams(); +tokenParams.clientId = "client@example.com"; + +Auth.TokenRequest tokenDetails = rest.auth.createTokenRequest(tokenParams, null); +``` + +```php +$rest = new Ably\AblyRest( + ['key' => '{{API_KEY}}'] +); + +$tokenRequest = $rest->auth->createTokenRequest( + ['clientId' => 'client@example.com'] +); +``` + +```go +rest, err := ably.NewREST( + ably.WithKey("{{API_KEY}}")) +if err != nil { + log.Fatalf("Error creating Ably client: %v", err) +} + +tokenParams := &ably.TokenParams{ClientID: "client@example.com"} +tokenRequest, _ := rest.Auth.CreateTokenRequest(tokenParams) +``` + +```flutter +final clientOptions = ably.ClientOptions( + key: '{{API_KEY}}', +); +final rest = ably.Rest(options: clientOptions); +const tokenParams = ably.TokenParams( + clientId: 'client@example.com' +); +final tokenRequest = rest.auth.createTokenRequest(tokenParams: tokenParams); +``` + + +Clients can pass this server-side generated `TokenRequest` to Ably to authenticate with Ably automatically. + +#### Ably Token + +Using an Ably SDK, an Ably Token is [requested by your servers](/docs/api/realtime-sdk/authentication#request-token) from Ably and then passed to the client-side SDK instance. The client-side SDK instance then uses that [Ably Token](/docs/api/realtime-sdk/authentication#tokens) to authenticate with Ably. This is an alternative approach for authentication that enables you to issue"Ably Tokens directly as opposed to providing Ably `TokenRequests` from your servers. + +The advantage for clients is that it saves one round trip request as they do not need to request an Ably Token themselves. The disadvantage is that your servers must communicate with Ably each time an Ably Token is required. + +The process used by Ably SDKs to authenticate with Ably using an Ably Token is illustrated in the following diagram: + +![Ably token auth process diagram](../../../../images/content/diagrams/Ably-Auth-Overview1.png) + +The following is an example of issuing an Ably Token from a server: + + +```javascript +const ably = new Ably.Rest({ key: '{{API_KEY}}' }); +const tokenDetails = await ably.auth.requestToken({ clientId: 'client@example.com' }); +``` + +```python +rest = AblyRest(key='{{API_KEY}}') +token_request_params = { + 'clientId': 'client@example.com', +} + +token_details = await rest.auth.request_token(token_params=token_request_params) +``` + +```java +ClientOptions options = new ClientOptions("{{API_KEY}}"); +AblyRest rest = new AblyRest(options); + +Auth.TokenParams tokenParams = new Auth.TokenParams(); +tokenParams.clientId = "client@example.com"; + +Auth.TokenDetails tokenDetails = rest.auth.requestToken(tokenParams, null); +``` + +```php +$rest = new Ably\AblyRest( + ['key' => '{{API_KEY}}'] +); + +$tokenDetails = $rest->auth->requestToken( + ['clientId' => 'client@example.com'] +); +``` + +```go +rest, err := ably.NewREST( + ably.WithKey("API_KEY")) +if err != nil { + log.Fatalf("Error creating Ably client: %v", err) +} + +tokenParams := &ably.TokenParams{ClientID: "client@example.com"} +tokenRequest, _ := rest.Auth.RequestToken(context.Background(), tokenParams) +``` + +```flutter +final clientOptions = ably.ClientOptions( + key: '{{API_KEY}}', +); +final rest = ably.Rest(options: clientOptions); + +const tokenParams = ably.TokenParams( + clientId: 'client@example.com', +); +final tokenDetails = await rest.auth.requestToken( + tokenParams: tokenParams +); +``` + + +### How to use token-based authentication? + +In your server expose a URL that your clients can make requests to get a new token from. It is recommended to use Ably SDKs because it provides functionality that automatically refreshes tokens near their expiration. + +To use automatic refresh of tokens, provide either an `authUrl` or an `authCallback`. When the token is near to expiry the `authUrl` or `authCallback` is invoked and a new token is automatically requested. + +An `authURL` is recommended for use with web-based clients as they can easily utilize cookies and other web-only features. For non-web clients, `authCallback` is the recommended strategy. + +#### authUrl + +You can specify an `authUrl` when you create the Ably client. For example: + + +```realtime_javascript +const realtime = new Ably.Realtime({ authUrl: '/auth' }); +``` + +```realtime_nodejs +const realtime = new Ably.Realtime({ authUrl: '/auth' }); +``` + +```realtime_ruby +realtime = Ably::Realtime.new(auth_url: '/auth') +``` + +```realtime_python + realtime = AblyRealtime(auth_url='/auth') +``` + +```realtime_java +ClientOptions options = new ClientOptions(); +options.authUrl = "/auth"; +AblyRealtime realtime = new AblyRealtime(options); +``` + +```realtime_objc +ARTClientOptions *options = [[ARTClientOptions alloc] init]; +options.authUrl = [NSURL URLWithString:@"/auth"]; +ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; +``` + +```realtime_swift +let options = ARTClientOptions() +options.authUrl = NSURL(string: "/auth") +let realtime = ARTRealtime(options: options) +``` + +```realtime_csharp +ClientOptions options = new ClientOptions(); +options.AuthUrl = new Uri("/auth"); +AblyRealtime realtime = new AblyRealtime(options); +``` + +```realtime_go +client, err := ably.NewRealtime(ably.WithAuthURL("/auth")) +``` + +```realtime_flutter +final clientOptions = ably.ClientOptions( + authUrl: '/auth' +); +final realtime = ably.Realtime(options: clientOptions); +``` + +```rest_javascript + const rest = new Ably.Rest({ authUrl: '/auth' }); +``` + +```rest_nodejs + const rest = new Ably.Rest({ authUrl: '/auth' }); +``` + +```rest_ruby + rest = Ably::Rest.new(auth_url: '/auth') +``` + +```rest_python + rest = AblyRest(auth_url='/auth') +``` + +```rest_php + $rest = new Ably\AblyRest(['authUrl' => '/auth']); +``` + +```rest_java + ClientOptions options = new ClientOptions(); + options.authUrl = "/auth"; + AblyRest rest = new AblyRest(options); +``` + +```rest_csharp + AblyRest rest = new AblyRest(new ClientOptions { AuthUrl = new Uri("/auth") }); +``` + +```rest_objc + ARTClientOptions *options = [[ARTClientOptions alloc] init]; + options.authUrl = [NSURL URLWithString:@"/auth"]; + ARTRest *rest = [[ARTRest alloc] initWithOptions:options]; +``` + +```rest_swift + let options = ARTClientOptions() + options.authUrl = NSURL(string: "/auth") + let rest = ARTRest(options: options) +``` + +```rest_go + client, err := ably.NewREST(ably.WithAuthURL("/auth")) +``` + +```rest_flutter +final clientOptions = ably.ClientOptions( + authUrl: '/auth' +); +final rest = ably.Rest(options: clientOptions); +``` + + +The client will obtain a token, JWT, or tokenRequest from the URL and use it to authenticate requests to Ably. Before token expiry, a request for a new token will be made automatically by the client to the `authUrl`. + +#### authCallback + +You can specify an authentication callback function when you create the Ably client. Inside `authCallback`, you can make a network request to your servers to generate the `tokenRequest`. For example: + + +```realtime_javascript +const ablyClient = new Realtime({ + authCallback: async (tokenParams, callback) => { + let tokenRequest; + try { + tokenRequest = await obtainTokenRequest(); // Make a network request to your server + } catch (err) { + callback(err, null); + return; + } + callback(null, tokenRequest); + } +}); +``` + +```realtime_nodejs +const ablyClient = new Realtime({ + authCallback: async (tokenParams, callback) => { + let tokenRequest; + try { + tokenRequest = await obtainTokenRequest(); // Make a network request to your server + } catch (err) { + callback(err, null); + return; + } + callback(null, tokenRequest); + } +}); +``` + +```realtime_python +realtime = AblyRealtime(auth_callback=get_token_request) + +async def get_token_request(*args, **kwargs): + create_token_request = await get_token_request() // Implement this function to get a token request from your server + + if create_token_request.status_code == 200: + token_request = create_token_request.json() + + return token_request + else: + raise Exception(f"Failed to retrieve token request: {create_token_request.status_code} - {create_token_request.text}") +``` + +```realtime_go +realtime, _ := ably.NewRealtime( + ably.WithAuthCallback(func(context.Context, ably.TokenParams) (ably.Tokener, error) { + createTokenRequest := getTokenRequest() // Implement this function to get a token request from your server + + if (createTokenRequest.StatusCode == 200) { + return createTokenRequest + } + + return "Failed to retrieve token request: '%v' - '%v'", createTokenRequest.StatusCode, createTokenRequest.Text + }) +) +``` + +```realtime_flutter +ably.ClientOptions clientOptions = ably.ClientOptions( + authCallback: (ably.TokenParams tokenParams) async { + // `createTokenRequest` should be implemented to communicate with user server + ably.TokenRequest tokenRequest = await createTokenRequest(tokenParams); + // `authCallback` has to return an instance of TokenRequest + return tokenRequest; + } +); + +ably.Realtime realtime = ably.Realtime(options: clientOptions); +``` + +```realtime_java +ClientOptions options = new ClientOptions(); + +options.authCallback = new Auth.TokenCallback() { + @Override + public Object getTokenRequest(Auth.TokenParams params) throws AblyException { + // Implement this function to get a token request from your server + return getTokenRequestFromServer(); + } +}; + +AblyRealtime realtime = new AblyRealtime(options); +``` + +```rest_javascript +const rest = new Ably.Rest({ + authCallback: (tokenParams, callback) => { + // implement your callback here + }, +}); +``` + +```rest_nodejs +const rest = new Ably.Rest({ + authCallback: (tokenParams, callback) => { + // implement your callback here + }, +}); +``` + +```rest_python +rest = AblyRest(auth_callback=get_token_request) + +async def get_token_request(*args, **kwargs): + create_token_request = await get_token_request() // Implement this function to get a token request from your server + + if create_token_request.status_code == 200: + token_request = create_token_request.json() + + return token_request + else: + raise Exception(f"Failed to retrieve token request: {create_token_request.status_code} - {create_token_request.text}") +``` + +```rest_java +ClientOptions options = new ClientOptions(); + +options.authCallback = new Auth.TokenCallback() { + @Override + public Object getTokenRequest(Auth.TokenParams params) throws AblyException { + // Implement this function to get a token request from your server + return getTokenRequestFromServer(); + } +}; + +AblyRest rest = new AblyRest(options); +``` + +```rest_php +$rest = new Ably\AblyRest( + [ + 'authCallback' => function(Ably\Models\TokenParams $params) { + $curl = curl_init('/auth'); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + $response = curl_exec($curl); + curl_close($curl); + + $tokenRequestData = json_decode($response, true); + $tokenRequest = new Ably\Models\TokenRequest($tokenRequestData); + + return $tokenRequest; + } + ] +); +``` + +```rest_go +rest, _ := ably.NewREST( + ably.WithAuthCallback(func(context.Context, ably.TokenParams) (ably.Tokener, error) { + createTokenRequest := getTokenRequest() // Implement this function to get a token request from your server + + if (createTokenRequest.StatusCode == 200) { + return createTokenRequest + } + + return "Failed to retrieve token request: '%v' - '%v'", createTokenRequest.StatusCode, createTokenRequest.Text + }) +) +``` + +```rest_flutter +ably.ClientOptions clientOptions = ably.ClientOptions( + authCallback: (ably.TokenParams tokenParams) async { + // `createTokenRequest` should be implemented to communicate with user server + ably.TokenRequest tokenRequest = await createTokenRequest(tokenParams); + // `authCallback` has to return an instance of TokenRequest + return tokenRequest; + } +); + +ably.Rest rest = ably.Rest(options: clientOptions); +``` + + +The [`tokenParams`](/docs/api/realtime-sdk/authentication#token-params) argument in `authCallback` is available for convenience, allowing you to see the capabilities, `clientId`, and other details requested by the client. However, `tokenParams` should not be trusted or used to generate the `tokenRequest` on the server side. Instead it is recommended that your `createTokenRequest` API authenticates clients separately, for example based on cookies, headers, or HTTP body. diff --git a/src/pages/docs/platform/auth/capabilities.mdx b/src/pages/docs/platform/auth/capabilities.mdx new file mode 100644 index 0000000000..806eef94e8 --- /dev/null +++ b/src/pages/docs/platform/auth/capabilities.mdx @@ -0,0 +1,897 @@ +--- +title: Identified clients and capabilities +meta_description: "Comprehensive guide to Ably's permissions system including API key capabilities, client identity management, and fine-grained access control." +redirect_from: + - /docs/auth/capabilities + - /docs/auth/identified-clients +--- + +## Identified clients + +When a client is authenticated and connected to Ably, it is considered to be an authenticated client. While an authenticated client has a means to authenticate with Ably, they do not necessarily have an identity. + +When a client is assigned a trusted identity, that is, a `clientId`, then they are considered to be an identified client. For all operations that client performs with the Ably service, their `clientId` field will be automatically populated and can be trusted by other clients. + +For example, assume you are building a chat application and want to allow clients to publish messages and be present on a channel. If each client is assigned a trusted identity by your server, such as a unique email address or UUID, then all other subscribed clients can trust any messages or presence events they receive in the channel as being from that client. No other clients are permitted to assume a `clientId` that they are not assigned in their Ably-compatible token. They are unable to masquerade as another `clientId`. + +### Assign a clientId + +There are three different ways a client can be identified with using a `clientId`: + +* A client claims a `clientId` when authenticating with an API key. +* A client is authenticating with a token issued for a specific `clientId`. +* A client claims a `clientId` when authenticating with a token that is issued for a wildcard `clientId`. + +When a client sets their own ID there is the possibility that they can pretend to be someone else. To prevent this, it is recommended that you embed a `clientId` in the token issued to your clients from your auth server. This ensures that the `clientId` is set by your auth server, and eliminates the chance of a client pretending to be someone else. + + + +#### Key auth + +You can use [key authentication](/docs/platform/auth/authentication#key-authentication) to allow a client to claim any `clientId` when they authenticate with Ably. As the assignation of the `clientId` is not handled by a server, it cannot be trusted to represent the genuine identity of the client. + +#### Token auth + +You can use [token authentication](/docs/platform/auth/authentication#token-based-authentication) to set an explicit `clientId` when creating or issuing a token. Clients using that token are restricted to operations for only that `clientId`, and all operations will implicitly contain that `clientId`. + +For example, when publishing a message, the `clientId` attribute of the message will be pre-populated with that `clientId`. Entering presence will also implicitly use that `clientId`. + +The following example demonstrates how to issue an [Ably TokenRequest](/docs/platform/auth/authentication#ably-tokens) with an explicit `clientId`: + + +```realtime_javascript + const realtime = new Ably.Realtime({ key: '{{API_KEY}}' }); + const tokenRequest = await realtime.auth.createTokenRequest({ clientId: 'Bob'}); +``` + +```realtime_nodejs + const realtime = new Ably.Realtime({ key: '{{API_KEY}}' }); + const tokenRequest = await realtime.auth.createTokenRequest({ clientId: 'Bob'}); +``` + +```realtime_ruby + realtime = Ably::Realtime.new(key: '{{API_KEY}}') + realtime.auth.createTokenRequest(client_id: 'Bob') do |token_request| + # ... issue the TokenRequest to a client ... + end +``` + +```realtime_python + realtime = AblyRealtime(key='{{API_KEY}}') + token_request = await realtime.auth.create_token_request({'client_id': 'Bob'}) + # ... issue the TokenRequest to a client ... +``` + +```realtime_java + ClientOptions options = new ClientOptions(); + options.key = "{{API_KEY}}"; + AblyRealtime realtime = new AblyRealtime(options); + TokenParams tokenParams = new TokenParams(); + tokenParams.clientId = "Bob"; + TokenRequest tokenRequest; + tokenRequest = realtime.auth.createTokenRequest(tokenParams, null); + /* ... issue the TokenRequest to a client ... */ +``` + +```realtime_csharp + AblyRealtime realtime = new AblyRealtime("{{API_KEY}}"); + TokenParams tokenParams = new TokenParams {ClientId = "Bob"}; + string tokenRequest = await realtime.Auth.CreateTokenRequestAsync(tokenParams); + /* ... issue the TokenRequest to a client ... */ +``` + +```realtime_objc + ARTRealtime *realtime = [[ARTRealtime alloc] initWithKey:@"{{API_KEY}}"]; + ARTTokenParams *tokenParams = [[ARTTokenParams alloc] initWithClientId:@"Bob"]; + [realtime.auth createTokenRequest:tokenParams options:nil + callback:^(ARTTokenRequest *tokenRequest NSError *error) { + // ... issue the TokenRequest to a client ... + }]; +``` + +```realtime_swift + let realtime = ARTRealtime(key: "{{API_KEY}}") + let tokenParams = ARTTokenParams(clientId: "Bob") + realtime.auth.createTokenRequest(tokenParams, options: nil) { tokenRequest, error in + // ... issue the TokenRequest to a client ... + } +``` + +```realtime_go +realtime, _ := ably.NewRealtime( + ably.WithKey("{{API_KEY}}")) +params := &ably.TokenParams{ + ClientID: "Bob", +} +tokenRequest, _ := realtime.Auth.CreateTokenRequest(params) +``` + +```realtime_flutter +final realtime = ably.Realtime(options: ably.ClientOptions(key: '{{API_KEY}}')); +final tokenRequest = await realtime.auth.createTokenRequest( + tokenParams: ably.TokenParams(clientId: 'Bob'), +); +``` + +```rest_javascript + const rest = new Ably.Rest({ key: '{{API_KEY}}' }); + const tokenRequest = await realtime.auth.createTokenRequest({ clientId: 'Bob'}); +``` + +```rest_nodejs + const rest = new Ably.Rest({ key: '{{API_KEY}}' }); + const tokenRequest = await realtime.auth.createTokenRequest({ clientId: 'Bob'}); +``` + +```rest_ruby + rest = Ably::Rest.new(key: '{{API_KEY}}') + token_request = rest.auth.create_token_request(client_id: 'Bob') + # ... issue the TokenRequest to a client ... +``` + +```rest_python + rest = AblyRest(key='{{API_KEY}}') + token_request = await rest.auth.create_token_request({'client_id': 'Bob'}) + # ... issue the TokenRequest to a client ... +``` + +```rest_php + $rest = new Ably\AblyRest( + ['key' => '{{API_KEY}}'] + ); + $tokenRequest = $rest->auth->createTokenRequest( + ['clientId' => 'Bob'] + ); + // ... issue the TokenRequest to a client ... +``` + +```rest_java + ClientOptions options = new ClientOptions(); + options.key = "{{API_KEY}}"; + AblyRest rest = new AblyRest(options); + TokenParams tokenParams = new TokenParams(); + tokenParams.clientId = "Bob"; + TokenRequest tokenRequest; + tokenRequest = rest.auth.createTokenRequest(tokenParams, null); + /* ... issue the TokenRequest to a client ... */ +``` + +```rest_csharp + AblyRest rest = new AblyRest(new ClientOptions {Key = "{{API_KEY}}"}); + TokenParams tokenParams = new TokenParams {ClientId = "Bob"}; + string tokenRequest = await rest.Auth.CreateTokenRequestAsync(tokenParams); + // ... issue the TokenRequest to a client ... +``` + +```rest_objc + ARTRest *rest = [[ARTRest alloc] initWithKey:@"{{API_KEY}}"]; + ARTTokenParams *tokenParams = [[ARTTokenParams alloc] initWithClientId:@"Bob"]; + [rest.auth createTokenRequest:tokenParams options:nil + callback:^(ARTTokenRequest *tokenRequest, NSError *error) { + // ... issue the TokenRequest to a client ... + }]; +``` + +```rest_swift + let rest = ARTRest(key: "{{API_KEY}}") + let tokenParams = ARTTokenParams(clientId: "Bob") + rest.auth.createTokenRequest(tokenParams, options: nil) { tokenRequest, error in + // ... issue the TokenRequest to a client ... + } +``` + +```rest_go +rest, _ := ably.NewREST( + ably.WithKey("{{API_KEY}}")) +params := &ably.TokenParams{ + ClientID: "Bob", +} +tokenRequest, _ := rest.Auth.CreateTokenRequest(params) +``` + +```rest_flutter +final rest = ably.Rest(options: ably.ClientOptions(key: '{{API_KEY}}')); +final tokenRequest = await rest.auth.createTokenRequest( + tokenParams: ably.TokenParams(clientId: 'Bob'), +); +``` + + +#### Wildcard token auth + +You can use [token authentication](/docs/platform/auth/authentication#token-based-authentication) to set a wildcard `clientId` using a value of `*` when creating a token. Clients are then able to assume any identity in their operations, such as when publishing a message or entering presence. + +### Unidentified clients + +If no `clientId` is provided when using [token authentication](/docs/platform/auth/authentication#token-based-authentication) then clients are not permitted to assume an identity and will be considered an unidentified client in all operations. Messages published will contain no `clientId` and those clients will not be permitted to enter the [presence](/docs/presence-occupancy/presence) set. + +## Capabilities + +API keys and Ably-compatible tokens, have a set of capabilities assigned to them that specify which operations (such as subscribe or publish) can be performed on which channels. + +API keys are long-lived, secret and typically not shared with clients. API key capabilities are configured using the [dashboard](https://ably.com/dashboard), or using the [Control API](/docs/platform/account/control-api). + +Ably-compatible tokens are designed to be shared with untrusted clients, are short-lived, and can be configured and issued programmatically. See [selecting an authentication mechanism](/docs/platform/auth/authentication#how-to-choose-authentication-method) to understand why token authentication is the preferred option in most scenarios. + +### Resource names and wildcards + +Capabilities are a map from resources to a list of [operations](#capability-operations). Each resource can match a single channel, for example, `channel`, or multiple channels using wildcards (`*`). + +Wildcards can only replace whole segments (segments are delimited by `:`) of the resource name. A wildcard at the end of the name can arbitrarily replace many segments. For example: + +* A resource of `*` will match any channel, but not queues and metachannels. +* A resource of `namespace:*` will match any channel in the `namespace` namespace, including `namespace:channel`, and `namespace:channel:other`. +* A resource of `foo:*:baz` will match `foo:bar:baz`, but not `foo:bar:bam:baz`. +* A resource of `foo:*` will match expressions such as `foo:bar`, `foo:bar:bam`, `foo:bar:bam:baz`, as the wildcard is at the end. +* A resource of `foo*` (without a colon) will only match the single channel literally called `foo*`. + +A resource can also be a queue, in which case it will start with `[queue]`, for example `[queue]appid-queuename`. This is unambiguous as channel names may not begin with a `[`. Similar wildcard rules apply, for example `[queue]*` will match all queues. + +A resource can also be a metachannel, in which case it will start with `[meta]`, for example `[meta]metaname`. This is unambiguous as channel names may not begin with a `[`. `[meta]*` will match all metachannels. Just `*` on its own will not: it will match all possible normal channels, but no metachannels. + +You can also have a resource name of `[*]*`, which will match all queues, all metachannels, and all channels. + +Wildcards are also supported for [operations](#capability-operations), by requesting an operations list of `['*']`. + +### Capability operations + +The following capability operations are available for API keys and issued tokens: + +| Operation | Description | +|-----------|-------------| +| **subscribe** | Can subscribe to messages, objects, and presence state change messages on channels, and get the presence set of a channel | +| **publish** | Can publish messages to channels | +| **presence** | Can register presence on a channel (enter, update and leave) | +| **object-subscribe** | Can subscribe to updates to objects on a channel | +| **object-publish** | Can update objects on a channel | +| **history** | Can retrieve message and presence state history on channels | +| **stats** | Can retrieve current and historical usage statistics for an app | +| **push-subscribe** | Can subscribe devices for push notifications | +| **push-admin** | Can manage device registrations and push subscriptions for all devices in an app | +| **channel-metadata** | Can get metadata for a channel, and enumerate channels | +| **privileged-headers** | Can set data in the privileged section of the [message extras](/docs/api/realtime-sdk/messages#extras) | + +Although most capabilities need to be enabled for the resource you're using them with, there are exceptions. The `stats` permission only does something when attached to the wildcard resource `'*'`, or a resource that contains that as a subset, such as `'[*]*'`, since stats are app-wide. + +The `channel-metadata` permission works both ways. When associated with a specific channel or set of channels it allows you to [query the metadata of a channel](/docs/metadata-stats/metadata/rest) to request its status. When associated with the wildcard resource `'*'` it takes on an additional meaning: as well as allowing channel status requests for all channels, it also allows you to [enumerate all active channels](/docs/metadata-stats/metadata/rest#enumerate). + + + +### API key capabilities + +An [Ably API key](/docs/platform/auth/authentication#key-authentication) can have a single set of permissions, applied to any number of [channels](/docs/channels) or [queues](/docs/platform/integrations/queues). + +You can also choose whether to restrict the API key to only channels, only [queues](/docs/platform/integrations/queues), or to match a set of channel or queue names. If you've chosen to restrict the API key to *selected channels and queues*, you can use a comma separated list of resources the API key can access, making use of wildcards to provide access to areas of your app. It is worth noting an API key will provide the same permissions to all resources it has access to. + +To view the capabilities for an existing API key: + +1. Sign into your [Ably dashboard](https://ably.com/dashboard). +2. Select the **API Keys** tab. +3. Click the **Settings** button for the key you want to check the capabilities for. + +### Token capabilities + +Ably Tokens and JWTs are issued from an existing API key and their capabilities can, at most, match the capabilities of the issuing API key. + +If an API key must be shared with a third party, then it is recommended that [the principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege) is considered, assigning only the capabilities needed by that third party. Thus, any Ably requests authenticated using that API key or Ably-compatible tokens associated with that API key, will be restricted to the capabilities assigned to the API key. + +Capabilities can be set when creating a token or token request, as shown in the following example: + + +```javascript + var tokenParams = { clientId: 'foo', capability: JSON.stringify(capability) }; + const tokenRequest = await ably.auth.createTokenRequest(tokenParams); +``` + +```python +token_params = { + 'client_id': 'foo', + 'capability': json.dumps(capability) +} + +token_request = await ably_rest.auth.create_token_request(token_params) +``` + +```php +$tokenParams = [ + 'clientId' => 'client@example.com', + 'capability' => json_encode($capability) +]; +$tokenRequest = $rest->auth->createTokenRequest($tokenParams); +``` + +```go +rest, _ := ably.NewREST( + ably.WithKey("xVLyHw.I2jW-g:dOTlhEt-nIubVAPMAUJnGv-_F8BZ7xNYnXdajpGaISg")) + +// Define the capability +capability := map[string][]string{ + "*": {"*"}, +} + +capabilityJSON, err := json.Marshal(capability) +if err != nil { + log.Fatalf("Failed to marshal capability: %v", err) +} + +// Define the token parameters +tokenParams := &ably.TokenParams{ + ClientID: "foo", + Capability: string(capabilityJSON), +} + +// Create a token request +tokenRequest, err := rest.Auth.CreateTokenRequest(tokenParams) +if err != nil { + log.Fatalf("Failed to create token request: %v", err) +} +``` + +```flutter +final tokenParams = ably.TokenParams( + clientId: 'foo', + capability: jsonEncode(capability), +); + +final tokenRequest = await rest.auth.createTokenRequest(tokenParams: tokenParams); +``` + + +#### Token capability determination + +The capabilities for tokens are determined based on those of the issuing API key and those requested by the token. + +##### Ably Token without capabilities + +If no capability is specified in an Ably `TokenRequest`, then the [Ably Token](/docs/platform/auth/authentication#token-based-authentication) will be given the full set of capabilities assigned to the issuing key. + +Using the following example, an API key exists with the listed capabilities. If an Ably Token is requested without specifying any capabilities then the `TokenRequest` is treated as requesting all capabilities, i.e. `{"[*]*":["*"]}`. This will result in the Ably Token receiving all the capabilities of the API key. + + +```javascript + // API key capabilities: + { + 'chat': ['publish', 'subscribe', 'presence'], + 'status': ['subscribe'] + } + + // Token request that doesn't specify any capabilities: + await auth.requestToken(tokenCallback) + + // Resulting token capabilities: + { + 'chat': ['publish', 'subscribe', 'presence'], + 'status': ['subscribe'] + } +``` + +```python +# API key capabilities: +# { +# "chat": ["publish", "subscribe", "presence"], +# "status": ["subscribe"] +# } + +// Token request that doesn't specify any capabilities: +token = await ably.auth.create_token_request( +{ + "clientId": "client@example.com", + 'ttl': 3600 * 1000, # ms +}) + +# Resulting token capabilities: +# { +# "chat": ["publish", "subscribe", "presence"], +# "status": ["subscribe"] +# } +``` + +```php +// API key capabilities: +//{ +// 'chat': ['publish', 'subscribe', 'presence'], +// 'status': ['subscribe'] +//} + +// Token request that doesn't specify any capabilities: +$tokenParams = [ + 'clientId' => 'client@example.com', + 'ttl' => 3600 * 1000, // ms +]; +$tokenRequest = $rest->auth->requestToken($tokenParams); + +// Resulting token capabilities: +//{ +// 'chat': ['publish', 'subscribe', 'presence'], +// 'status': ['subscribe'] +//} +``` + +```go +// API key capabilities: +// { +// "chat": ["publish", "subscribe", "presence"], +// "status": ["subscribe"] +// } + +rest, _ := ably.NewREST( + ably.WithKey("{{API_KEY}}")) + +// Define the token parameters +tokenParams := &ably.TokenParams{ + ClientID: "client@example.com", + TTL: 3600 * 1000, /* time of expiration in ms (an hour) */ +} + +// Create a token request +tokenRequest, err := rest.Auth.CreateTokenRequest(tokenParams) +if err != nil { + log.Fatalf("Failed to create token request: %v", err) +} + +// Resulting token capabilities: +// { +// "chat": ["publish", "subscribe", "presence"], +// "status": ["subscribe"] +// } +``` + +```flutter +// API key capabilities: +// { +// 'chat': ['publish', 'subscribe', 'presence'], +// 'status': ['subscribe'] +// } + +final tokenRequest = await realtime.auth.requestToken(tokenParams: tokenParams); + +// Resulting token capabilities: +// { +// 'chat': ['publish', 'subscribe', 'presence'], +// 'status': ['subscribe'] +// } +``` + + +##### Ably Token with intersection of capabilities + +If a set of capabilities are requested, then the Ably Token will be assigned the intersection of the requested capability and the capability of the issuing key. + +Using the following example, an API key exists with the listed capabilities. If an [Ably Token](/docs/auth/token#tokens) is requested and specifies a set of capabilities, then the resulting token will only receive those capabilities that intersect. The capabilities of a token cannot exceed those of the issuing API key. + + +```javascript + // API key capabilities: + { + 'chat:*': ['publish', 'subscribe', 'presence'], + 'status': ['subscribe', 'history'], + 'alerts': ['subscribe'] + } + + // Token request that specifies capabilities: + const tokenDetails = await auth.requestToken({ capability: { + 'chat:bob': ['subscribe'], // only 'subscribe' intersects + 'status': ['*'], // '*'' intersects with 'subscribe' + 'secret': ['publish', 'subscribe'] // key does not have access to 'secret' channel + }}); + + // Resulting token capabilities: + { + 'chat:bob': ['subscribe'], + 'status': ['subscribe', 'history'] + } +``` + +```python +# API key capabilities: +# { +# "chat:*": ["publish", "subscribe", "presence"], +# "status": ["subscribe", "history"], +# "alerts": ["subscribe"] +# } + +# Token request that specifies capabilities: +capabilities = { + "chat:bob": ["subscribe"], # only "subscribe" intersects + "status": ["*"], # "*" intersects with "subscribe" + "secret": ["publish", "subscribe"] # key does not have access to "secret" channel +} + +token_details = await ably_rest.auth.request_token({ + 'capability': json.dumps(capabilities) +}) + +# Resulting token capabilities: +# { +# "chat:bob": ["subscribe"], +# "status": ["subscribe", "history"] +# } +``` + +```php +/** + * API key capabilities: + * { + * 'chat:*': ['publish', 'subscribe', 'presence'], + * 'status': ['subscribe', 'history'], + * 'alerts': ['subscribe'] + * } + */ + +// Token request that specifies capabilities: +$capabilities = [ + 'chat:bob' => ['subscribe'], // only 'subscribe' intersects + 'status' => ['*'], // '*' intersects with 'subscribe' + 'secret' => ['publish', 'subscribe'] // key does not have access to 'secret' channel +]; + +$tokenDetails = $rest + ->auth + ->requestToken( + ['capability' => json_encode($capabilities)] + ); + +/** + * Resulting token capabilities: + * { + * 'chat:bob': ['subscribe'], + * 'status': ['subscribe', 'history'] + * } + */ +``` + +```go +// API key capabilities: +// { +// "chat:*": ["publish", "subscribe", "presence"], +// "status": ["subscribe", "history"], +// "alerts": ["subscribe"] +// } + +// Token request that specifies capabilities: +rest, _ := ably.NewREST( + ably.WithKey("{{API_KEY}}")) + +// Define the capabilities +capabilities := map[string][]string{ + "chat:bob": {"subscribe"}, + "status": {"*"}, + "secret": {"publish", "subscribe"}, +} + +capabilitiesJSON, err := json.Marshal(capabilities) +if err != nil { + log.Fatalf("Failed to marshal capabilities: %v", err) +} + +// Define the token parameters +tokenParams := &ably.TokenParams{ + Capability: string(capabilitiesJSON), +} + +// Request a token +tokenDetails, err := rest.Auth.RequestToken(context.Background(), tokenParams) +if err != nil { + log.Fatalf("Failed to request token: %v", err) +} + +// Resulting token capabilities: +// { +// "chat:bob": ["subscribe"], +// "status": ["subscribe", "history"] +// } +``` + +```flutter +// API key capabilities: +// { +// 'chat:bob': ['subscribe'], +// 'status': ['*'], +// 'secret': ['publish', 'subscribe'] +// } + +final tokenParams = ably.TokenParams( + capability: jsonEncode({ + 'chat:bob': ['subscribe'], + 'status': ['*'], + 'secret': ['publish', 'subscribe'] + }), +); + +final tokenDetails = await rest.auth.requestToken(tokenParams: tokenParams); + +// Resulting token capabilities: +// { +// 'chat:bob': ['subscribe'], +// 'secret': ['publish','subscribe'] +// 'status': ['subscribe', 'history'] +// } +``` + + +##### Ably Token with incompatible capabilities + +If a set of capabilities are requested, and the intersection between those and the API key's capabilities is empty, then the `TokenRequest` will result in an error. + +Using the following example, an API key exists with the listed capabilities. If an [Ably Token](/docs/auth/token#tokens) is requested that specifies a set of capabilities, and there is no intersection between the capabilities of the issuing API key and requested token, then the token request will be rejected. In the following example, the callback will be returned with an error. + + +```javascript + // API key capabilities: + { + 'chat': ['*'] + } + + // Token request that specifies capabilities: + const tokenDetails = await auth.requestToken({ capability: { + 'status': ['*'] + }}); +``` + +```python +# API key capabilities: +# { +# "chat": ["*"] +# } + +token_details = await ably_rest.auth.request_token({ + 'capability': json.dumps({ + { + "status": ["*"] + } + }) +}) +``` + +```php +/** + * API key capabilities: + * { + * 'chat': ['*'] + * } + */ + +// Token request that specifies capabilities: +$tokenDetails = $rest + ->auth + ->requestToken( + ['capability' => json_encode(['status' => ['*']])] + ); +``` + +```go +// API key capabilities: +// { +// "chat": ["*"] +// } + +rest, _ := ably.NewREST( + ably.WithKey("{{API_KEY}}")) + +// Define the capabilities +capabilities := map[string][]string{ + "status": {"*"}, +} + +capabilitiesJSON, err := json.Marshal(capabilities) +if err != nil { + log.Fatalf("Failed to marshal capabilities: %v", err) +} + +// Define the token parameters +tokenParams := &ably.TokenParams{ + Capability: string(capabilitiesJSON), +} + +// Request a token +tokenDetails, err := rest.Auth.RequestToken(context.Background(), tokenParams) +if err != nil { + log.Fatalf("Failed to request token: %v", err) +} +``` + +```flutter +// API key capabilities: +// { +// 'status': ['*'] +// } +final tokenParams = ably.TokenParams( + capability: jsonEncode({ + 'status': ['*'] + }), +); + +final tokenDetails = await realtime.auth.requestToken(tokenParams: tokenParams); +``` + + +##### Ably JWT capability determination + +Capabilities are determined for [Ably JWTs](/docs/auth/token#jwt) in the following way: + +* The capabilities granted to an Ably JWT will be the intersection of the capabilities within the Ably JWT and the capabilities of the associated API key. +* If the set of capabilities within the Ably JWT have no intersection with the capabilities of the API key, then an error will instead be returned. + +### Custom restrictions on channels + +It is possible for JWTs to contain authenticated claims for users that can be used to allow or disallow certain interactions in your channels. + +Messages can be annotated with trusted metadata copied from the client's authentication token by Ably servers. Clients are unable to directly publish messages with user claim metadata, and claims contained within the authentication token are signed to prevent tampering. Claims can be scoped to individual channels or to namespaces of [channels](/docs/channels). The most specific user claim will be added to the message as part of the `extras` object. Note that this does not apply to presence or metadata messages. + +To set the trusted fields you need to include `ably.channel.*` in your JWT authentication payload, for example: + + +```javascript + const claims = { + 'sub': '1234567890', + 'name': 'John Doe', + 'x-ably-capability': <...>, + 'x-ably-clientId': <...>, + 'ably.channel.chat1': 'admin', // the user is an admin for the chat1 channel + 'ably.channel.chat:*': 'moderator', // the user is a moderator in channels within the chat namespace + 'ably.channel.*': 'guest', // the user is a guest in all other channels + } +``` + +```python +claims = { + "sub": "1234567890", + "name": "John Doe", + "x-ably-capability": "<...>", + "x-ably-clientId": "<...>", + "ably.channel.chat1": "admin", # the user is an admin for the chat1 channel + "ably.channel.chat:*": "moderator", # the user is a moderator in channels within the chat namespace + "ably.channel.*": "guest" # the user is a guest in all other channels +} +``` + +```php +$claims = [ + 'sub' => '1234567890', + 'name' => 'John Doe', + 'x-ably-capability' => '<...>', + 'x-ably-clientId' => '<...>', + 'ably.channel.chat1' => 'admin', // the user is an admin for the chat1 channel + 'ably.channel.chat =>*' => 'moderator', // the user is a moderator in channels within the chat namespace + 'ably.channel.*' => 'guest' // the user is a guest in all other channels +]; +``` + +```go +claims := map[string]interface{}{ + "sub": "1234567890", + "name": "John Doe", + "x-ably-capability": "<...>", + "x-ably-clientId": "<...>", + "ably.channel.chat1": "admin", + "ably.channel.chat:*": "moderator", + "ably.channel.*": "guest", +} +``` + +```flutter +final claims = { + 'sub': '1234567890', + 'name': 'John Doe', + 'x-ably-capability': '<...>', + 'x-ably-clientId': '<...>', + 'ably.channel.chat1': 'admin', + 'ably.channel.chat:*': 'moderator', + 'ably.channel.*': 'guest', +}; +``` + + +The claims from the token are copied into messages, allowing them to be checked for permission: + + +```javascript + const fromModerator = (message) => { + const userClaim = message.extras && message.extras.userClaim; + return (userClaim && userClaim == 'moderator'); + } +``` + +```python +def from_moderator(message): + user_claim = message.get('extras', {}).get('userClaim') + return user_claim == 'moderator' if user_claim else False +``` + +```go +func fromModerator(message map[string]interface{}) bool { + // Check if 'extras' exists and is a map + if extras, ok := message["extras"].(map[string]interface{}); ok { + // Check if 'userClaim' exists and is a string + if userClaim, ok := extras["userClaim"].(string); ok { + return userClaim == "moderator" + } + } + return false +} +``` + +```flutter +bool fromModerator(Message message) { + final userClaim = message.extras['userClaim']; + return userClaim != null && userClaim == 'moderator'; +} +``` + + +### Using JWT for per connection publish rate limits + +JWTs may specify publish rate limits for a user on particular channels. These limits can be used to prevent any individual user from sending an excessive number of messages in a short period of time. + +An example use case is in a large live chat where you may wish to limit users to posting messages no more than once every 10 seconds. + +Rate limits can be scoped to individual channels or to [channel namespaces](/docs/channels#namespaces). Note that the rate limit with the most specific scope will be applied to the user. + +To set rate limits for individual connections, include `ably.limits.publish.perAttachment.maxRate.` in your JWT authentication payload. The value of this property sets how many messages can be published per second to a channel, or namespace. For example, a value of `5` restricts the rate to 5 messages per second. A value of `0.1` restricts the rate to 1 message every 10 seconds. + +The following is an example of setting different rate limits for different channels: + + +```javascript + const claims = { + 'sub': '1234567890', + 'name': 'John Doe', + 'x-ably-capability': <...>, + 'x-ably-clientId': <...>, + 'ably.limits.publish.perAttachment.maxRate.chat1': 10, // the user can publish 10 messages per second in channel chat1 + 'ably.limits.publish.perAttachment.maxRate.chat:*': 0.1 // the user can publish a message every 10 seconds in all channels within the chat namespace + } +``` + +```python +claims = { + "sub": "1234567890", + "name": "John Doe", + "x-ably-capability": "<...>", + "x-ably-clientId": "<...>", + "ably.limits.publish.perAttachment.maxRate.chat1": 10, # the user can publish 10 messages per second in channel chat1 + "ably.limits.publish.perAttachment.maxRate.chat:*": 0.1 # the user can publish a message every 10 seconds in all channels within the chat namespace +} +``` + +```php +$claims = [ + 'sub' => '1234567890', + 'name' => 'John Doe', + 'x-ably-capability' => '<...>', + 'x-ably-clientId' => '<...>', + 'ably.limits.publish.perAttachment.maxRate.chat1' => 10, // the user can publish 10 messages per second in channel chat1 + 'ably.limits.publish.perAttachment.maxRate.chat:*' => 0.1 // the user can publish a message every 10 seconds in all channels within the chat namespace +] +``` + +```go +claims := map[string]interface{}{ + "sub": "1234567890", + "name": "John Doe", + "x-ably-capability": "<...>", + "x-ably-clientId": "<...>", + "ably.limits.publish.perAttachment.maxRate.chat1": 10.0, // the user can publish 10 messages per second in channel chat1 + "ably.limits.publish.perAttachment.maxRate.chat:*": 0.1, // the user can publish a message every 10 seconds in all channels within the chat namespace +} +``` + +```flutter +final claims = { + 'sub': '1234567890', + 'name': 'John Doe', + 'x-ably-capability': '<...>', // Replace with actual capability + 'x-ably-clientId': '<...>', // Replace with actual client ID + 'ably.limits.publish.perAttachment.maxRate.chat1': 10, // the user can publish 10 messages per second in channel chat1 + 'ably.limits.publish.perAttachment.maxRate.chat:*': 0.1 // the user can publish a message every 10 seconds in all channels within the chat namespace +}; +``` + diff --git a/src/pages/docs/platform/auth/index.mdx b/src/pages/docs/platform/auth/index.mdx new file mode 100644 index 0000000000..bcd2e9c937 --- /dev/null +++ b/src/pages/docs/platform/auth/index.mdx @@ -0,0 +1,77 @@ +--- +title: Authentication Overview +meta_description: "Ably authentication supports key-based and token-based authentication for secure access across all Ably products and services." +redirect_from: + - /docs/auth + - /docs/auth/overview + - /docs/core-features/authentication + - /docs/general/authentication +--- + +Ably provides secure authentication mechanisms across all of its products and services including Pub/Sub, Chat, LiveObjects, Spaces, and Asset Tracking. Before any client or server can interact with Ably services, it must authenticate using an Ably API key or token. + +## Authentication Options + +There are two methods of authentication available, key and token authentication: + +### Key Authentication + +Key authentication uses your Ably API key directly and is designed for trusted server environments. + +**When to use:** +- Server-side applications +- Secure environments where API keys can be safely stored +- Backend services and servers + +**Benefits:** +- Simple to implement +- No token expiration to manage +- Full access to API key capabilities + +### Token Authentication + +Token authentication uses short-lived tokens issued by your servers for client applications. **This is the recommended approach for client-side authentication.** + +**When to use:** +- Web browsers and mobile applications +- Any untrusted client environment +- When you need fine-grained access control + +**Benefits:** +- Enhanced security - tokens expire automatically +- Fine-grained permissions per token +- Ability to revoke access if needed +- Identity management through `clientId` + +## API Keys + +Every Ably app can have multiple API keys, each with different capabilities and permissions. API keys are the foundation of Ably's authentication system. + +### API Key Structure + +An Ably API key (`I2E_JQ.OqUdfg:EVKVTCBlzLBPYJiCZTsIW_pqylJ9WVRB5K9P19Ap1y0`) has three components: + +1. **App ID** (`I2E_JQ`) - Identifies your Ably application +2. **Key ID** (`OqUdfg`) - Identifies the specific API key +3. **Key Secret** (`EVKVTCBlzLBPYJiCZTsIW_pqylJ9WVRB5K9P19Ap1y0`) - Private secret for authentication + +**Important:** The key secret should never be shared with untrusted parties or embedded in client applications. + +## Client Identity + +Ably supports client identity through the `clientId` parameter, which allows you to: + +- Identify specific users or devices +- Implement presence features +- Apply user-specific permissions +- Track user activity across sessions + +The `clientId` can be set during authentication and is included in all messages and presence events from that client. + +## Security Best Practices + +1. **Never use key authentication client-side** - Always use token authentication for browsers and mobile apps +2. **Set appropriate token expiry times** - Balance security with user experience +3. **Use `clientId` for user identity** - This enables presence and user-specific features +4. **Implement proper token refresh** - Ensure seamless user experience when tokens expire +5. **Apply principle of least privilege** - Grant only the minimum required permissions diff --git a/src/pages/docs/platform/auth/revocation.mdx b/src/pages/docs/platform/auth/revocation.mdx new file mode 100644 index 0000000000..ac5501522d --- /dev/null +++ b/src/pages/docs/platform/auth/revocation.mdx @@ -0,0 +1,244 @@ +--- +title: Token revocation +meta_description: "Token revocation is a mechanism that enables an app to invalidate authentication tokens." +--- + +Token revocation is a mechanism that enables an app to invalidate authentication tokens. This invalidation can be used to force specified clients to re-obtain a token, and subsequently enables the app to modify the rights granted to clients, or to decline to re-issue a token. Token revocation can be enforced immediately or postponed by 30 seconds, allowing the client the opportunity to request a new token (see `allowReauthMargin` under [Revoke a token](#revoke)). By default `allowReauthMargin` is set to false, meaning token revocation is near immediate. Setting this value to true would postpone the revocation by 30 seconds. + +The main use case of token revocation is as a method to combat malicious clients. In cases where the client behaves safely, you don't need token revocation - your code can instruct the connection to Ably to close, or detach from a channel it shouldn't have access to. But if you want to prevent any chance of a user obtaining the token before it is disconnected, and using it in their own custom client to gain access to Ably, then token revocation can be used. + +Without token revocation, in order to restrict potential rogue access, you would make tokens with a short TTL (say 10 minutes) and then simply not issue a new token to a suspect client. Unfortunately, reducing the TTL increases the load on your authentication server, so token revocation can provide a more scalable solution to the issue of rogue clients. There is however a performance cost to using this feature. + +## Revocable tokens + +Tokens are revocable if they are created from an API key that has token revocation enabled. This sets the `revocableTokens` attribute to `true` for the API key. + +To enable token revocation for an API key: + +1. Sign into your [Ably dashboard](https://ably.com/accounts/any). +2. Select the **API Keys** tab. +3. Open the **Settings** for an existing API key or **Create a new API key**. +4. Check the **Revocable tokens** field: + +![Token revocation settings](../../../images/content/screenshots/token-revocation.png) + +**Note:** Token revocation can only revoke tokens that were issued by an API key that had revocable tokens enabled before the token was issued. + +Tokens created from an API key with revocable tokens enabled are restricted to a maximum TTL of 1 hour; an attempt to create, or present, a token that is revocable, but has a TTL of longer than 1 hr, is rejected with an error. + +Initial connections to Ably and REST requests can incur a small latency cost when using revocable tokens. + +## Revoke a token + +The Ably REST API contains the following endpoint for [revoking tokens](/docs/api/rest-api#revoke-tokens): + + +```text +POST /keys/{{API_KEY_NAME}}/revokeTokens +``` + + +Where the `API_KEY_NAME` is `appId.keyId`. + +In that request, the request body has the following form: + + +```json +{ + targets: [ "clientId:client1@example.com", "clientId:client2@example.com" ], + issuedBefore: 1636556083000, + allowReauthMargin: true +} +``` + + +The request body has the following properties: + +- **targets**: An array of [target specifier](#target-specifiers) strings. +- **issuedBefore**: Optional number (Unix timestamp in milliseconds); if not specified it is set to the current time. The token revocation only applies to tokens issued before this timestamp. A request with an `issuedBefore` in the future, or more than an hour in the past will be rejected. +- **allowReauthMargin**: Optional boolean. The `allowReauthMargin` boolean permits a token renewal cycle to take place without needing established connections to be dropped, by postponing enforcement to 30 seconds in the future, and sending any existing connections a hint to obtain (and upgrade the connection to use) a new token. It defaults to `false`, meaning that the effect is near-immediate. + +When invoking the revocation API, a client must prove possession of the key used to issue the tokens to be revoked. You can't use a key to revoke a token issued from a different key. This can be done by using basic authentication, using the API key itself. + +There can be a maximum of 100 target specifiers in a single request. + +The token revocation API is rate-limited, so there is a maximum global aggregate rate of revocation requests per app. The rate is configurable by Ably at the application level. As part of the process of enabling revocation on an app Ably will ask you what the maximum rate of token revocations on that app needs to be, and then provision that capacity. The decision is not permanent, it can be changed by Ably on request at any time. + +Note that revocation does not permanently forbid a client, connection, `clientId` or channel. It invalidates token credentials issued before a certain point in time. Ultimately, the app that issues tokens is responsible for whether or not a client is able to continue to interact with the Ably service. + +## Revocation targets + +A revocation request must include one or more target specifiers which define the token(s) that are affected by the request. Where multiple target specifiers are included, this is equivalent to making multiple independent revocation requests, each for a single target. The tokens affected by a request are those that match at least one of the target specifiers included in the request. + +All target specifiers are formatted as: `key:`. + +By default, supported target specifiers are: + +* clientId +* revocationKey + +The following target specifier can also be enabled for you by Ably, should your use case require it: + +* channel + +### clientId + +This target specifier will match tokens that have the specified `clientId`. + +For example, `targets: ["clientId:client1"]` matches tokens containing the `clientId` of `client1`. + +A sample request to revoke an Ably token based on `clientId`, using the REST interface of an SDK: + + +```rest_javascript +const Ably = require('ably'); + +const ablyRest = new Ably.Rest({ key: '{{API_KEY}}' }); +const requestBody = { targets: ['clientId:client1@example.com'] }; + +const revocationResponse = await ablyRest.request( + 'post', + '/keys/{{API_KEY_NAME}}/revokeTokens', + 3, + null, + requestBody +); + +if (!revocationResponse.success) { + console.log('An error occurred; err = ' + revocationResponse.errorMessage); +} else { + console.log('Success! status code was ' + revocationResponse.statusCode); +} +``` + +```rest_python +rest = AblyRest(key='{{API_KEY}}') + +request_body = { + 'targets': ['clientId:client1@example.com'] +} + +response = await rest.request('POST', '/keys/{{API_KEY_NAME}}/revokeTokens', 3, body=request_body) + +if not response.is_success: + print('An error occurred; err = ' + response.error_message) +else: + print('Success! status code was ' + str(response.status_code)) + +await rest.close() +``` + +```rest_php +$rest = new Ably\AblyRest( + ['key' => '{{API_KEY}}'] +); + +$requestBody = ['targets' => ['clientId:client@example.com']]; + +$response = $rest->request( + 'POST', + '/keys/{{API_KEY_NAME}}/revokeTokens', + [], + $requestBody +); + +if (!$response->success) { + echo('An error occurred; err = ' . $response->errorMessage); +} else { + echo('Success! status code was ' . strval($response->statusCode)); +} +``` + + +Where the `API_KEY_NAME` is `appId.keyId`. + +In this example, the token with the unique client ID `client1@example.com` would be revoked. + +### revocationKey + +Designating a revocation key for a token, or a group of tokens, enables the revocation process to be used at any level of granularity, depending on the needs of the application. This method of revocation only works for JWTs, as traditional tokens do not contain this field. + +To designate a revocation key, include the following additional claim in the JWT: + +- **x-ably-revocation-key**: a string used to identify which token(s) to revoke in the revocation request. + +This target specifier will match tokens that have the specified `revocationKey`. + +For example: `targets: ["revocationKey:users.group1"]` matches tokens containing the `revocationKey` of `users.group1`. + +A sample request to revoke a JWT based on `revocationKey`, using the REST interface of an SDK: + + +```rest_javascript +const Ably = require('ably'); + +const ablyRest = new Ably.Rest({ key: '{{API_KEY}}' }) +const requestBody = { targets: ['revocationKey:users.group1@example.com'] }; +const revocationResponse = await ablyRest.request( + 'post', + '/keys/{{API_KEY_NAME}}/revokeTokens', + 3, + null, + requestBody, + null +); + +if (!revocationResponse.success) { + console.log('An error occurred; err = ' + revocationResponse.errorMessage); +} else { + console.log('Success! status code was ' + revocationResponse.statusCode); +} +``` + +```rest_python +rest = AblyRest(key='{{API_KEY}}') + +request_body = { + 'targets': ['revocationKey:users.group1@example.com'] +} + +response = await rest.request('POST', '/keys/{{API_KEY_NAME}}/revokeTokens', 3, body=request_body) + +if not response.is_success: + print('An error occurred; err = ' + response.error_message) +else: + print('Success! status code was ' + str(response.status_code)) + +await rest.close() +``` + +```rest_php +$rest = new Ably\AblyRest( + ['key' => '{{API_KEY}}'] +); + +$requestBody = ['targets' => ['revocationKey:users.group1@example.com']]; + +$response = $rest->request( + 'POST', + '/keys/{{API_KEY_NAME}}/revokeTokens', + [], + $requestBody +); + +if (!$response->success) { + echo('An error occurred; err = ' . $response->errorMessage); +} else { + echo('Success! status code was ' . strval($response->statusCode)); +} +``` + + +In this example, all users that have been assigned the revocation key `users.group1@example.com` would have their tokens revoked. + +### channel + +This target specifier will match tokens that exactly match one of the resource names present in the token [capabilities](/docs/platform/auth/capabilities). Note that this is not the same thing as revoking all tokens that have access to the channel. + +For example, a token with a capability of `{"foo:*": ["*"]}` will be revoked by a target of `channel:foo:*`, but a revocation to `channel:*:*` will have no effect (even though that is a superset of the capabilities of `foo:*`), and nor will `channel:foo:bar` (even for connections using the token to attach to that particular channel). It must be the exact string used in the token capabilities (which may be inherited from key capabilities). + +Note that the channel target specifier can be enabled by Ably should your use case require it. + +## Revocation errors + +System error responses arising from the use of an invalidated token will include an Ably `ErrorInfo` with a `code` of 40141 and an informative error message. This includes REST API error responses, and Ably connection `disconnected` state changes. Such a token error is non-fatal, which means that client libraries can react to that error by triggering a token renewal where the given library options allow (that is, if an `authCallback` or `authURL` is specified).