Skip to content

Commit

Permalink
Merge branch 'main' into fix/broken-fallbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
sacOO7 authored Apr 18, 2024
2 parents 4c2ab86 + cb7fb4a commit f4bcd48
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 102 deletions.
48 changes: 36 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,32 +384,56 @@ foreach (var presenceMessage in nextPage.Items)
}
```

### Using the AuthCallback
### Authentication
- It is recommended to use `ABLY_KEY` at server side. Check [official ably auth documentation](https://ably.com/docs/auth) for more info.
- `ABLY_KEY` should not be exposed at client side where it can be used for malicious purposes.
- Server can use `ABLY_KEY` for initializing the `AblyRest` instance.

A callback to obtain a signed `TokenRequest` string or a `TokenDetails` instance.
```csharp
var rest = new AblyRest("API_KEY");
```
- Token requests are issued by your servers and signed using your private API key.
- This is the preferred method of authentication as no secrets are ever shared, and the token request can be issued to trusted clients without communicating with Ably.
```csharp
// e.g. ASP.Net server endpoint
app.MapGet("/token", async() => {
string tokenRequest = await rest.Auth.CreateTokenRequestAsync();
return Content(tokenRequest, "application/json"); // make sure to set `contentType` as json.
});
```
- You can also return JWT string token signed using `ABLY_KEY` as per [official ably JWT doc](https://ably.com/tutorials/jwt-authentication).

### Using the Token auth at client side

To use `AuthCallback` create a `ClientOptions` instance and assign an appropriate delegate to the `AuthCallback` property and pass the `ClientOptions` to a new `AblyRealtime` instance.
- Create `ClientOptions` instance with `AuthCallback` property

```csharp
var options = new ClientOptions
{
AuthCallback = async tokenParams =>
{
// Return a 'TokenDetails'/'TokenRequest' object or a token string .
// Typically this method would wrap a request to your web server.
return await GetTokenDetailsOrTokenRequestStringFromYourServer();
// Return serialized tokenRequest string or 'IO.Ably.TokenRequest' object
return await TokenRequestStringFromYourServer(tokenParams); // tokenRequest will be used to obtain token from ably server.
}
};
var client = new AblyRealtime(options);
```

### Generate a TokenRequest

Token requests are issued by your servers and signed using your private API key. This is the preferred method of authentication as no secrets are ever shared, and the token request can be issued to trusted clients without communicating with Ably.

- If JWT token is returned by server
```csharp
TokenRequest tokenRequest = await client.Auth.CreateTokenRequestObjectAsync();
var options = new ClientOptions
{
AuthCallback = async tokenParams =>
{
// Return serialized jwttokenstring returned from server
string jwtToken = await getJwtTokenFromServer(tokenParams);
int expiresIn = 3600; // assuming jwtToken has 1 hr expiry
return new TokenDetails(jwtToken) {
Expires = DateTimeOffset.UtcNow.AddSeconds(expiresIn)
}; // jwt token will directly be used to authenticate with ably server.
}
};
```
- Check [official token auth documentation](https://ably.com/docs/auth/token?lang=csharp) for more information.

### Fetching your application's stats

Expand Down
153 changes: 91 additions & 62 deletions src/IO.Ably.Shared/AblyAuth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,31 +302,33 @@ public virtual async Task<TokenDetails> RequestTokenAsync(TokenParams tokenParam
TokenRequest postData = null;
if (authOptions.AuthCallback != null)
{
var shouldCatch = true;
bool shouldCatch = true;
try
{
var callbackResult = await authOptions.AuthCallback(tokenParams);

switch (callbackResult)
if (callbackResult == null)
{
case null:
throw new AblyException("AuthCallback returned null", ErrorCodes.ClientAuthProviderRequestFailed);
case string token:
if (string.IsNullOrEmpty(token))
{
throw new AblyException("AuthCallback returned empty string", ErrorCodes.ClientAuthProviderRequestFailed);
}

return new TokenDetails(token);
case TokenDetails details:
return details;
case TokenRequest tokenRequest:
postData = tokenRequest;
request.Url = $"/keys/{tokenRequest.KeyName}/requestToken";
break;
default:
shouldCatch = false;
throw new AblyException($"AuthCallback returned an unsupported type ({callbackResult.GetType()}. Expected either TokenDetails or TokenRequest", ErrorCodes.ClientAuthProviderRequestFailed, HttpStatusCode.BadRequest);
throw new AblyException("AuthCallback returned null", ErrorCodes.ClientAuthProviderRequestFailed);
}

if (callbackResult is TokenDetails)
{
return callbackResult as TokenDetails;
}

if (callbackResult is TokenRequest || callbackResult is string)
{
postData = GetTokenRequest(callbackResult);
request.Url = $"/keys/{postData.KeyName}/requestToken";
}
else
{
shouldCatch = false;
throw new AblyException(
$"AuthCallback returned an unsupported type ({callbackResult.GetType()}. Expected either TokenDetails or TokenRequest",
ErrorCodes.ClientAuthProviderRequestFailed,
HttpStatusCode.BadRequest);
}
}
catch (Exception ex) when (shouldCatch)
Expand Down Expand Up @@ -453,6 +455,31 @@ private void NotifyClientIdIfChanged(string oldClientId)
}
}

#pragma warning disable SA1204 // Static elements should appear before instance elements
private static TokenRequest GetTokenRequest(object callbackResult)
#pragma warning restore SA1204 // Static elements should appear before instance elements
{
if (callbackResult is TokenRequest)
{
return callbackResult as TokenRequest;
}

try
{
var result = JsonHelper.Deserialize<TokenRequest>((string)callbackResult);
if (result == null)
{
throw new AblyException(new ErrorInfo($"AuthCallback returned a string which can't be converted to TokenRequest. ({callbackResult})."));
}

return result;
}
catch (Exception e)
{
throw new AblyException(new ErrorInfo($"AuthCallback returned a string which can't be converted to TokenRequest. ({callbackResult})."), e);
}
}

private async Task<AblyResponse> CallAuthUrl(AuthOptions mergedOptions, TokenParams @params)
{
var url = mergedOptions.AuthUrl;
Expand Down Expand Up @@ -530,13 +557,25 @@ public async Task<TokenDetails> AuthorizeAsync(TokenParams tokenParams = null, A
return CurrentToken;
}

public TokenDetails Authorize(TokenParams tokenParams = null, AuthOptions options = null)
{
return AsyncHelper.RunSync(() => AuthorizeAsync(tokenParams, options));
}

[Obsolete("This method will be removed in the future, please replace with a call to AuthorizeAsync")]
public async Task<TokenDetails> AuthoriseAsync(TokenParams tokenParams = null, AuthOptions options = null)
{
Logger.Warning("AuthoriseAsync is deprecated and will be removed in the future, please replace with a call to AuthorizeAsync");
return await AuthorizeAsync(tokenParams, options);
}

[Obsolete("This method will be removed in the future, please replace with a call to Authorize")]
public TokenDetails Authorise(TokenParams tokenParams = null, AuthOptions options = null)
{
Logger.Warning("Authorise is deprecated and will be removed in the future, please replace with a call to Authorize.");
return AsyncHelper.RunSync(() => AuthorizeAsync(tokenParams, options));
}

private void SetCurrentTokenParams(TokenParams authTokenParams)
{
CurrentTokenParams = authTokenParams.Clone();
Expand All @@ -551,30 +590,6 @@ private void SetCurrentAuthOptions(AuthOptions options)
}
}

/// <summary>
/// Create a signed token request based on known credentials
/// and the given token params. This would typically be used if creating
/// signed requests for submission by another client.
/// </summary>
/// <param name="tokenParams"><see cref="TokenParams"/>. If null a token request is generated from options passed when the client was created.</param>
/// <param name="authOptions"><see cref="AuthOptions"/>. If null the default AuthOptions are used.</param>
/// <returns>signed token request.</returns>
public async Task<TokenRequest> CreateTokenRequestObjectAsync(TokenParams tokenParams, AuthOptions authOptions)
{
authOptions = authOptions ?? CurrentAuthOptions ?? Options;
tokenParams = tokenParams ?? CurrentTokenParams ?? TokenParams.WithDefaultsApplied();

if (string.IsNullOrEmpty(authOptions.Key))
{
throw new AblyException("No key specified", ErrorCodes.InvalidCredentials, HttpStatusCode.Unauthorized);
}

await SetTokenParamsTimestamp(authOptions, tokenParams);

var apiKey = authOptions.ParseKey();
return new TokenRequest(Now).Populate(tokenParams, apiKey.KeyName, apiKey.KeySecret);
}

private TokenAuthMethod GetTokenAuthMethod()
{
if (Options.AuthCallback != null)
Expand Down Expand Up @@ -657,35 +672,49 @@ public TokenDetails RequestToken(TokenParams tokenParams = null, AuthOptions opt
return AsyncHelper.RunSync(() => RequestTokenAsync(tokenParams, options));
}

public TokenDetails Authorize(TokenParams tokenParams = null, AuthOptions options = null)
/// <summary>
/// Create a signed token request based on known credentials
/// and the given token params. This would typically be used if creating
/// signed requests for submission by another client.
/// </summary>
/// <param name="tokenParams"><see cref="TokenParams"/>. If null a token request is generated from options passed when the client was created.</param>
/// <param name="authOptions"><see cref="AuthOptions"/>. If null the default AuthOptions are used.</param>
/// <returns>signed token request.</returns>
public async Task<string> CreateTokenRequestAsync(TokenParams tokenParams, AuthOptions authOptions)
{
return AsyncHelper.RunSync(() => AuthorizeAsync(tokenParams, options));
}
authOptions = authOptions ?? CurrentAuthOptions ?? Options;
tokenParams = tokenParams ?? CurrentTokenParams ?? TokenParams.WithDefaultsApplied();

[Obsolete("This method will be removed in the future, please replace with a call to Authorize")]
public TokenDetails Authorise(TokenParams tokenParams = null, AuthOptions options = null)
{
Logger.Warning("Authorise is deprecated and will be removed in the future, please replace with a call to Authorize.");
return AsyncHelper.RunSync(() => AuthorizeAsync(tokenParams, options));
if (string.IsNullOrEmpty(authOptions.Key))
{
throw new AblyException("No key specified", ErrorCodes.InvalidCredentials, HttpStatusCode.Unauthorized);
}

await SetTokenParamsTimestamp(authOptions, tokenParams);

var apiKey = authOptions.ParseKey();
var tokenRequest = new TokenRequest(Now).Populate(tokenParams, apiKey.KeyName, apiKey.KeySecret);

return JsonHelper.Serialize(tokenRequest);
}

public TokenRequest CreateTokenRequestObject(TokenParams tokenParams = null, AuthOptions authOptions = null)
public string CreateTokenRequest(TokenParams tokenParams = null, AuthOptions authOptions = null)
{
return AsyncHelper.RunSync(() => CreateTokenRequestObjectAsync(tokenParams, authOptions));
return AsyncHelper.RunSync(() => CreateTokenRequestAsync(tokenParams, authOptions));
}

[Obsolete("This method will be removed in a future version, please use CreateTokenRequestObjectAsync instead")]
public async Task<string> CreateTokenRequestAsync(TokenParams tokenParams, AuthOptions authOptions)
[Obsolete("This method will be removed in a future version, please use CreateTokenRequestAsync instead")]
public async Task<TokenRequest> CreateTokenRequestObjectAsync(TokenParams tokenParams, AuthOptions authOptions)
{
Logger.Warning("CreateTokenRequest is deprecated and will be removed in the future, please use CreateTokenRequestObject instead");
var tokenRequest = await CreateTokenRequestObjectAsync(tokenParams, authOptions);
return JsonHelper.Serialize(tokenRequest);
Logger.Warning("CreateTokenRequestObject is deprecated and will be removed in the future, please use CreateTokenRequest instead");
var tokenRequest = await CreateTokenRequestAsync(tokenParams, authOptions);
return JsonHelper.Deserialize<TokenRequest>(tokenRequest);
}

[Obsolete("This method will be removed in a future version, please use CreateTokenRequestObject instead")]
public string CreateTokenRequest(TokenParams tokenParams = null, AuthOptions authOptions = null)
[Obsolete("This method will be removed in a future version, please use CreateTokenRequest instead")]
public TokenRequest CreateTokenRequestObject(TokenParams tokenParams = null, AuthOptions authOptions = null)
{
return AsyncHelper.RunSync(() => CreateTokenRequestAsync(tokenParams, authOptions));
return AsyncHelper.RunSync(() => CreateTokenRequestObjectAsync(tokenParams, authOptions));
}
}
}
4 changes: 2 additions & 2 deletions src/IO.Ably.Shared/IAblyAuth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ public interface IAblyAuth
/// <param name="tokenParams"><see cref="TokenParams"/>. If null a token request is generated from options passed when the client was created.</param>
/// <param name="authOptions"><see cref="AuthOptions"/>. If null the default AuthOptions are used.</param>
/// <returns>serialized signed token request.</returns>
[Obsolete("This method will be removed in a future version, please use CreateTokenRequestObjectAsync instead")]
Task<string> CreateTokenRequestAsync(TokenParams tokenParams = null, AuthOptions authOptions = null);

/// <summary>
Expand All @@ -68,6 +67,7 @@ public interface IAblyAuth
/// <param name="tokenParams"><see cref="TokenParams"/>. If null a token request is generated from options passed when the client was created.</param>
/// <param name="authOptions"><see cref="AuthOptions"/>. If null the default AuthOptions are used.</param>
/// <returns>signed token request.</returns>
[Obsolete("This method will be removed in a future version, please use CreateTokenRequestAsync instead")]
Task<TokenRequest> CreateTokenRequestObjectAsync(TokenParams tokenParams = null, AuthOptions authOptions = null);

/// <summary>
Expand Down Expand Up @@ -107,7 +107,6 @@ public interface IAblyAuth
/// <param name="tokenParams"><see cref="TokenParams"/>. If null a token request is generated from options passed when the client was created.</param>
/// <param name="authOptions"><see cref="AuthOptions"/>. If null the default AuthOptions are used.</param>
/// <returns>serialized signed token request.</returns>
[Obsolete("This method will be removed in a future version, please use CreateTokenRequestObject instead")]
string CreateTokenRequest(TokenParams tokenParams = null, AuthOptions authOptions = null);

/// <summary>
Expand All @@ -117,6 +116,7 @@ public interface IAblyAuth
/// <param name="tokenParams"><see cref="TokenParams"/>. If null a token request is generated from options passed when the client was created.</param>
/// <param name="authOptions"><see cref="AuthOptions"/>. If null the default AuthOptions are used.</param>
/// <returns>signed token request.</returns>
[Obsolete("This method will be removed in a future version, please use CreateTokenRequest instead")]
TokenRequest CreateTokenRequestObject(TokenParams tokenParams = null, AuthOptions authOptions = null);
}
}
6 changes: 3 additions & 3 deletions src/IO.Ably.Tests.Shared/AuthTests/AuthSandboxSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ public async Task RealTimeWithAuthUrl_WhenTokenExpired_And_WithServerTime_Should
});
await Task.Delay(2000);
// This makes sure we get server time
_ = ((AblyAuth)mainClient.Auth).CreateTokenRequestObject();
_ = ((AblyAuth)mainClient.Auth).CreateTokenRequest();

await mainClient.StatsAsync();
((AblyAuth)mainClient.Auth).CurrentToken.Should().NotBeSameAs(token);
Expand Down Expand Up @@ -373,7 +373,7 @@ static void AuthCallbackOptions(ClientOptions options, TestEnvironmentSettings s
static void InvalidTokenOptions(ClientOptions options, TestEnvironmentSettings settings)
{
options.AutoConnect = false;
options.AuthCallback = (tokenParams) => Task.FromResult<object>(string.Empty);
options.AuthCallback = (tokenParams) => Task.FromResult<object>("invalid:token");
}

await TestConnectingBecomesDisconnected("With invalid AuthUrl connection becomes Disconnected", AuthUrlOptions);
Expand Down Expand Up @@ -869,7 +869,7 @@ public async Task TokenAuthCallbackWithTokenRequestReturned_ShouldBeAbleToGetATo
var tokenClient = await GetRestClient(protocol);
var authCallbackClient = await GetRestClient(protocol, options =>
{
options.AuthCallback = async tokenParams => await tokenClient.Auth.CreateTokenRequestObjectAsync(new TokenParams { ClientId = "*" });
options.AuthCallback = async tokenParams => await tokenClient.Auth.CreateTokenRequestAsync(new TokenParams { ClientId = "*" });
options.Environment = settings.Environment;
options.UseBinaryProtocol = protocol == Defaults.Protocol;
});
Expand Down
Loading

0 comments on commit f4bcd48

Please sign in to comment.