1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-15 23:47:25 +08:00
osu-lazer/osu.Game/Online/API/IAPIProvider.cs
Bartłomiej Dach 3006bae0d8
Send client-generated session GUID for identification purposes
This is the first half of a change that *may* fix
https://github.com/ppy/osu/issues/26338 (it definitely fixes *one case*
where the issue happens, but I'm not sure if it will cover all of them).

As described in the issue thread, using the `jti` claim from the JWT
used for authorisation seemed like a decent idea. However, upon closer
inspection the scheme falls over badly in a specific scenario where:

1. A client instance connects to spectator server using JWT A.

2. At some point, JWT A expires, and is silently rotated by the game in
   exchange for JWT B.

   The spectator server knows nothing of this, and continues to only
   track JWT A, including the old `jti` claim in said JWT.

3. At some later point, the client's connection to one of the spectator
   server hubs drops out. A reconnection is automatically attempted,
   *but* it is attempted using JWT B.

   The spectator server was not aware of JWT B until now, and said JWT
   has a different `jti` claim than the old one, so to the spectator
   server, it looks like a completely different client connecting, which
   boots the user out of their account.

This PR adds a per-session GUID which is sent in a HTTP header on every
connection attempt to spectator server. This GUID will be used instead
of the `jti` claim in JWTs as a persistent identifier of a single user's
single lazer session, which bypasses the failure scenario described
above.

I don't think any stronger primitive than this is required. As far as I
can tell this is as strong a protection as the JWT was (which is to say,
not *very* strong), and doing this removes a lot of weird complexity
that would be otherwise incurred by attempting to have client ferry all
of its newly issued JWTs to the server so that it can be aware of them.
2024-07-17 15:56:41 +02:00

165 lines
5.7 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Game.Localisation;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Users;
namespace osu.Game.Online.API
{
public interface IAPIProvider
{
/// <summary>
/// The local user.
/// </summary>
IBindable<APIUser> LocalUser { get; }
/// <summary>
/// The user's friends.
/// </summary>
IBindableList<APIUser> Friends { get; }
/// <summary>
/// The current user's activity.
/// </summary>
IBindable<UserActivity> Activity { get; }
/// <summary>
/// The current user's online statistics.
/// </summary>
IBindable<UserStatistics?> Statistics { get; }
/// <summary>
/// The language supplied by this provider to API requests.
/// </summary>
Language Language { get; }
/// <summary>
/// Retrieve the OAuth access token.
/// </summary>
string AccessToken { get; }
/// <summary>
/// Used as an identifier of a single local lazer session.
/// Sent across the wire for the purposes of concurrency control to spectator server.
/// </summary>
Guid SessionIdentifier { get; }
/// <summary>
/// Returns whether the local user is logged in.
/// </summary>
bool IsLoggedIn { get; }
/// <summary>
/// The last username provided by the end-user.
/// May not be authenticated.
/// </summary>
string ProvidedUsername { get; }
/// <summary>
/// The URL endpoint for this API. Does not include a trailing slash.
/// </summary>
string APIEndpointUrl { get; }
/// <summary>
/// The root URL of the website, excluding the trailing slash.
/// </summary>
string WebsiteRootUrl { get; }
/// <summary>
/// The version of the API.
/// </summary>
int APIVersion { get; }
/// <summary>
/// The last login error that occurred, if any.
/// </summary>
Exception? LastLoginError { get; }
/// <summary>
/// The current connection state of the API.
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
/// </summary>
IBindable<APIState> State { get; }
/// <summary>
/// Queue a new request.
/// </summary>
/// <param name="request">The request to perform.</param>
void Queue(APIRequest request);
/// <summary>
/// Perform a request immediately, bypassing any API state checks.
/// </summary>
/// <remarks>
/// Can be used to run requests as a guest user.
/// </remarks>
/// <param name="request">The request to perform.</param>
void Perform(APIRequest request);
/// <summary>
/// Perform a request immediately, bypassing any API state checks.
/// </summary>
/// <remarks>
/// Can be used to run requests as a guest user.
/// </remarks>
/// <param name="request">The request to perform.</param>
Task PerformAsync(APIRequest request);
/// <summary>
/// Attempt to login using the provided credentials. This is a non-blocking operation.
/// </summary>
/// <param name="username">The user's username.</param>
/// <param name="password">The user's password.</param>
void Login(string username, string password);
/// <summary>
/// Provide a second-factor authentication code for authentication.
/// </summary>
/// <param name="code">The 2FA code.</param>
void AuthenticateSecondFactor(string code);
/// <summary>
/// Log out the current user.
/// </summary>
void Logout();
/// <summary>
/// Sets Statistics bindable.
/// </summary>
void UpdateStatistics(UserStatistics newStatistics);
/// <summary>
/// Constructs a new <see cref="IHubClientConnector"/>. May be null if not supported.
/// </summary>
/// <param name="clientName">The name of the client this connector connects for, used for logging.</param>
/// <param name="endpoint">The endpoint to the hub.</param>
/// <param name="preferMessagePack">Whether to use MessagePack for serialisation if available on this platform.</param>
IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true);
/// <summary>
/// Accesses the <see cref="INotificationsClient"/> used to receive asynchronous notifications from web.
/// </summary>
INotificationsClient NotificationsClient { get; }
/// <summary>
/// Creates a <see cref="IChatClient"/> instance to use in order to chat.
/// </summary>
IChatClient GetChatClient();
/// <summary>
/// Create a new user account. This is a blocking operation.
/// </summary>
/// <param name="email">The email to create the account with.</param>
/// <param name="username">The username to create the account with.</param>
/// <param name="password">The password to create the account with.</param>
/// <returns>Any errors encoutnered during account creation.</returns>
RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password);
}
}