Skip to content

Commit

Permalink
Implement REST token revocation
Browse files Browse the repository at this point in the history
As described by spec at commit 18ef967. Documentation based on
sdk-api-reference repo at commit 652ef2f.

Note that the tests which revoke tokens do so using a random clientId.
This is to prevent the revocations performed in one test from having an
effect on tokens issued in a subsequent test. (I observed this
happening, and Simon confirmed that it's possible for a token issued
very shortly after a revocation to be affected by that revocation.)

Resolves #989.
  • Loading branch information
lawrence-forooghian committed Aug 2, 2023
1 parent 3e05117 commit 88bac53
Show file tree
Hide file tree
Showing 3 changed files with 557 additions and 0 deletions.
86 changes: 86 additions & 0 deletions ably.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,69 @@ declare namespace Types {
error: ErrorInfo;
}

/**
* The `TokenRevocationOptions` interface describes the additional options accepted by the following methods:
*
* - {@link AuthCallbacks.revokeTokens}
* - {@link AuthPromise.revokeTokens}
*/
interface TokenRevocationOptions {
/**
* A Unix timestamp in milliseconds where only tokens issued before this time are revoked. The default is the current time. Requests with an `issuedBefore` in the future, or more than an hour in the past, will be rejected.
*/
issuedBefore?: number;
/**
* If true, 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. The default is `false`, meaning that the effect is near-immediate.
*/
allowReauthMargin?: boolean;
}

/**
* Describes which tokens should be affected by a token revocation request.
*/
interface TokenRevocationTargetSpecifier {
/**
* The type of token revocation target specifier. Valid values include `clientId`, `revocationKey` and `channel`.
*/
type: string;
/**
* The value of the token revocation target specifier.
*/
value: string;
}

/**
* Contains information about the result of a successful token revocation request for a single target specifier.
*/
interface TokenRevocationSuccessResult {
/**
* The target specifier.
*/
target: string;
/**
* The time at which the token revocation will take effect, as a Unix timestamp in milliseconds.
*/
appliesAt: number;
/**
* A Unix timestamp in milliseconds. Only tokens issued earlier than this time will be revoked.
*/
issuedBefore: number;
}

/**
* Contains information about the result of an unsuccessful token revocation request for a single target specifier.
*/
interface TokenRevocationFailureResult {
/**
* The target specifier.
*/
target: string;
/**
* Describes the reason for which token revocation failed for the given `target` as an {@link ErrorInfo} object.
*/
error: ErrorInfo;
}

// Common Listeners
/**
* A standard callback format used in most areas of the callback API.
Expand Down Expand Up @@ -2186,6 +2249,18 @@ declare namespace Types {
* @param callback - A function which, upon success, will be called with a {@link TokenDetails} object. Upon failure, the function will be called with information about the error.
*/
requestToken(callback?: tokenDetailsCallback): void;
/**
* Revokes the tokens specified by the provided array of {@link TokenRevocationTargetSpecifier}s. Only tokens issued by an API key that had revocable tokens enabled before the token was issued can be revoked. See the [token revocation docs](https://ably.com/docs/core-features/authentication#token-revocation) for more information.
*
* @param specifiers - An array of {@link TokenRevocationTargetSpecifier} objects.
* @param options - A set of options which are used to modify the revocation request.
* @param callback - A function which, upon success, will be called with a {@link Types.BatchResult} containing information about the result of the token revocation request for each provided [`TokenRevocationTargetSpecifier`]{@link TokenRevocationTargetSpecifier}. Upon failure, the function will be called with information about the error.
*/
revokeTokens(
specifiers: TokenRevocationTargetSpecifier[],
options?: TokenRevocationOptions,
callback?: StandardCallback<BatchResult<TokenRevocationSuccessResult | TokenRevocationFailureResult>>
): void;
}

/**
Expand Down Expand Up @@ -2216,6 +2291,17 @@ declare namespace Types {
* @returns A promise which, upon success, will be fulfilled with a {@link TokenDetails} object. Upon failure, the promise will be rejected with an {@link ErrorInfo} object which explains the error.
*/
requestToken(TokenParams?: TokenParams, authOptions?: AuthOptions): Promise<TokenDetails>;
/**
* Revokes the tokens specified by the provided array of {@link TokenRevocationTargetSpecifier}s. Only tokens issued by an API key that had revocable tokens enabled before the token was issued can be revoked. See the [token revocation docs](https://ably.com/docs/core-features/authentication#token-revocation) for more information.
*
* @param specifiers - An array of {@link TokenRevocationTargetSpecifier} objects.
* @param options - A set of options which are used to modify the revocation request.
* @returns A promise which, upon success, will be fulfilled with a {@link Types.BatchResult} containing information about the result of the token revocation request for each provided [`TokenRevocationTargetSpecifier`]{@link TokenRevocationTargetSpecifier}. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error.
*/
revokeTokens(
specifiers: TokenRevocationTargetSpecifier[],
options?: TokenRevocationOptions
): Promise<BatchResult<TokenRevocationSuccessResult | TokenRevocationFailureResult>>;
}

/**
Expand Down
78 changes: 78 additions & 0 deletions src/common/lib/client/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ import ClientOptions from '../../types/ClientOptions';
import HttpMethods from '../../constants/HttpMethods';
import HttpStatusCodes from 'common/constants/HttpStatusCodes';
import Platform from '../../platform';
import Resource from './resource';

type BatchResult<T> = API.Types.BatchResult<T>;
type TokenRevocationTargetSpecifier = API.Types.TokenRevocationTargetSpecifier;
type TokenRevocationOptions = API.Types.TokenRevocationOptions;
type TokenRevocationSuccessResult = API.Types.TokenRevocationSuccessResult;
type TokenRevocationFailureResult = API.Types.TokenRevocationFailureResult;
type TokenRevocationResult = BatchResult<TokenRevocationSuccessResult | TokenRevocationFailureResult>;

const MAX_TOKEN_LENGTH = Math.pow(2, 17);
function noop() {}
Expand Down Expand Up @@ -1054,6 +1062,76 @@ class Auth {
static isTokenErr(error: IPartialErrorInfo) {
return error.code && error.code >= 40140 && error.code < 40150;
}

revokeTokens(
specifiers: TokenRevocationTargetSpecifier[],
options?: TokenRevocationOptions,
callback?: API.Types.StandardCallback<TokenRevocationResult>
): void;
revokeTokens(
specifiers: TokenRevocationTargetSpecifier[],
options?: TokenRevocationOptions
): Promise<TokenRevocationResult>;
revokeTokens(
specifiers: TokenRevocationTargetSpecifier[],
optionsOrCallbackArg?: TokenRevocationOptions | API.Types.StandardCallback<TokenRevocationResult>,
callbackArg?: API.Types.StandardCallback<TokenRevocationResult>
): void | Promise<TokenRevocationResult> {
if (useTokenAuth(this.client.options)) {
throw new ErrorInfo('Cannot revoke tokens when using token auth', 40162, 401);
}

const keyName = this.client.options.keyName!;

let resolvedOptions: TokenRevocationOptions;

if (typeof optionsOrCallbackArg === 'function') {
callbackArg = optionsOrCallbackArg;
resolvedOptions = {};
} else {
resolvedOptions = optionsOrCallbackArg ?? {};
}

if (callbackArg === undefined) {
if (this.client.options.promises) {
return Utils.promisify(this, 'revokeTokens', [specifiers, resolvedOptions]);
}
callbackArg = noop;
}

const callback = callbackArg;

const requestBodyDTO = {
targets: specifiers.map((specifier) => `${specifier.type}:${specifier.value}`),
...resolvedOptions,
};

const format = this.client.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json,
headers = Utils.defaultPostHeaders(this.client.options, format);

if (this.client.options.headers) Utils.mixin(headers, this.client.options.headers);

const requestBody = Utils.encodeBody(requestBodyDTO, format);
Resource.post(
this.client,
`/keys/${keyName}/revokeTokens`,
requestBody,
headers,
{ newBatchResponse: 'true' },
null,
(err, body, headers, unpacked) => {
if (err) {
// TODO remove this type assertion after fixing https://github.com/ably/ably-js/issues/1405
callback(err as API.Types.ErrorInfo);
return;
}

const batchResult = (unpacked ? body : Utils.decodeBody(body, format)) as TokenRevocationResult;

callback(null, batchResult);
}
);
}
}

export default Auth;
Loading

0 comments on commit 88bac53

Please sign in to comment.