refactor(auth): redesign Steam audience and web domain authorization model

- Rename AuthorizedDomains to WebDomains
- Introduce explicit Steam audience constants and web audience mappings
- Improve Steam token/domain resolution and cookie installation flow
- Allow mobile-issued tokens with "web" audience to be used for web domains
- Align authorization logic closer to actual Steam audience behavior
- Rename cookie APIs from Add* to Set* where appropriate
This commit is contained in:
achiez
2026-05-12 16:36:37 +03:00
parent ce55005d44
commit 30cf049f23
7 changed files with 136 additions and 124 deletions
@@ -62,7 +62,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
SetStatus(newStatus); SetStatus(newStatus);
ClientHandler.CookieContainer.ClearAllCookies(); ClientHandler.CookieContainer.ClearAllCookies();
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(sessionData); ClientHandler.CookieContainer.SetSteamMobileCookies(sessionData);
} }
+1 -1
View File
@@ -38,7 +38,7 @@ public static class MaClient
{ {
ClientHandler.CookieContainer.ClearAllCookies(); ClientHandler.CookieContainer.ClearAllCookies();
if (account == null) return; if (account == null) return;
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData); ClientHandler.CookieContainer.SetSteamMobileCookies(account.SessionData);
Proxy.SetData(account.Proxy?.Data); Proxy.SetData(account.Proxy?.Data);
} }
@@ -30,7 +30,7 @@ public partial class SessionHandler //API
//Trigger PropertyChanged event for PortableMaClient handling session updated from MaClient //Trigger PropertyChanged event for PortableMaClient handling session updated from MaClient
mafile.SetSessionData(mafile.SessionData); mafile.SetSessionData(mafile.SessionData);
await Storage.UpdateMafileAsync(mafile); await Storage.UpdateMafileAsync(mafile);
chp.Handler.CookieContainer.SetSteamMobileCookiesWithMobileToken(mafile.SessionData); chp.Handler.CookieContainer.SetSteamMobileCookies(mafile.SessionData);
} }
public static async Task LoginAgain(HttpClientHandlerPair chp, Mafile mafile, string password, bool savePassword) public static async Task LoginAgain(HttpClientHandlerPair chp, Mafile mafile, string password, bool savePassword)
@@ -54,33 +54,20 @@ public static class AdmissionHelper
return; return;
} }
container.SetSteamRefreshToken(sessionData.RefreshToken);
AddRefreshToken(container, sessionData.RefreshToken);
var community = SteamDomains.GetDomainUri(SteamDomain.Community); var community = SteamDomains.GetDomainUri(SteamDomain.Community);
container.Add(community, new Cookie(SESSION_ID_COOKIE_NAME, sessionData.SessionId, "/")); container.Add(community, new Cookie(SESSION_ID_COOKIE_NAME, sessionData.SessionId, "/"));
container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage, "/")); container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage, "/"));
TransferCommunityCookies(container); TransferCommunityCookies(container);
foreach (var domain in SteamDomains.AuthDomains) foreach (var domain in SteamDomains.WebDomains)
{ {
var token = sessionData.GetToken(domain); var token = sessionData.GetToken(domain);
if (token == null) continue; if (token == null) continue;
AddTokenCookie(container, token.Value); container.SetSteamAccessToken(token.Value);
} }
} }
public static void SetDomainCookie(this CookieContainer container, SteamDomain domain, SteamAuthToken token)
{
var uri = SteamDomains.GetDomainUri(domain);
foreach (var cookie in container.GetCookies(uri)
.Where(c => !c.Expired && c.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)))
{
cookie.Expired = true;
}
AddTokenCookie(container, token);
}
/// <summary> /// <summary>
/// Clear and set new session /// Clear and set new session
/// </summary> /// </summary>
@@ -95,70 +82,22 @@ public static class AdmissionHelper
return; return;
} }
AddRefreshToken(container, mobileSession.RefreshToken); container.SetSteamRefreshToken(mobileSession.RefreshToken);
var community = SteamDomains.GetDomainUri(SteamDomain.Community); var community = SteamDomains.GetDomainUri(SteamDomain.Community);
container.Add(community, new Cookie("steamid", mobileSession.SteamId.Steam64.ToString())); container.Add(community, new Cookie("steamid", mobileSession.SteamId.Steam64.ToString()));
container.Add(community, new Cookie(SESSION_ID_COOKIE_NAME, mobileSession.SessionId)); container.Add(community, new Cookie(SESSION_ID_COOKIE_NAME, mobileSession.SessionId));
container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage)); container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage));
TransferCommunityCookies(container); TransferCommunityCookies(container);
foreach (var domain in SteamDomains.AuthDomains) foreach (var domain in SteamDomains.WebDomains)
{ {
var token = mobileSession.GetToken(domain); var token = mobileSession.GetToken(domain);
if (token == null) continue; if (token == null) continue;
AddTokenCookie(container, token.Value); var domainUri = SteamDomains.GetDomainUri(domain);
container.SetSteamAccessTokenUnsafe(token.Value, domainUri);
} }
} }
/// <summary>
/// Clear and set new session. Not recommended. Uses <see cref="IMobileSessionData.GetMobileToken()" /> for domain
/// <see cref="SteamDomain.Community" /> instead of its own cookie. It's okay to use it only for confirmations. But
/// Market, Trading and other pages won't be authorized
/// </summary>
public static void SetSteamMobileCookiesWithMobileToken(this CookieContainer container,
IMobileSessionData? mobileSession,
string setLanguage = "english")
{
container.ClearSteamCookies(setLanguage);
container.AddMinimalMobileCookies();
if (mobileSession == null)
{
TransferCommunityCookies(container);
return;
}
AddRefreshToken(container, mobileSession.RefreshToken);
var community = SteamDomains.GetDomainUri(SteamDomain.Community);
container.Add(community, new Cookie("steamid", mobileSession.SteamId.Steam64.ToString()));
container.Add(community, new Cookie(SESSION_ID_COOKIE_NAME, mobileSession.SessionId));
container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage));
TransferCommunityCookies(container);
var domainCookieSet = false;
foreach (var domain in SteamDomains.AllDomains)
{
var token = mobileSession.GetToken(domain);
if (token == null || token.Value.IsExpired) continue;
if (domain == SteamDomain.Community)
domainCookieSet = true;
AddTokenCookie(container, token.Value);
}
var mobileToken = mobileSession.GetMobileToken();
if (!domainCookieSet && mobileToken is {IsExpired: false})
{
var domain = SteamDomains.GetDomainUri(SteamDomain.Community);
container.Add(domain, new Cookie(ACCESS_COOKIE_NAME, mobileToken.Value.SignedToken)
{
HttpOnly = true,
Secure = true,
Expires = mobileToken.Value.Expires.ToLocalDateTime()
});
}
}
public static void AddMinimalMobileCookies(this CookieContainer container) public static void AddMinimalMobileCookies(this CookieContainer container)
{ {
@@ -229,27 +168,93 @@ public static class AdmissionHelper
} }
} }
public static void AddRefreshToken(CookieContainer container, SteamAuthToken token) /// <summary>
/// Sets a Steam refresh token as a cookie to the specified cookie container.
/// </summary>
/// <remarks>
/// The added cookie will have its expiration set according to the token's expiration time. This
/// method is typically used to enable authenticated requests to Steam services that require a refresh
/// token.
/// </remarks>
/// <param name="container">The cookie container to which the Steam refresh token cookie will be added. Cannot be null.</param>
/// <param name="token">
/// The Steam authentication token to add as a refresh cookie. Must be of type Refresh or
/// MobileRefresh.
/// </param>
/// <exception cref="ArgumentException">Thrown if the token is not of type Refresh or MobileRefresh.</exception>
public static void SetSteamRefreshToken(this CookieContainer container, SteamAuthToken token)
{ {
if (token.Type is not (SteamAccessTokenType.Refresh or SteamAccessTokenType.MobileRefresh)) if (token.Type is not (SteamAccessTokenType.Refresh or SteamAccessTokenType.MobileRefresh))
throw new ArgumentException( throw new ArgumentException(
$"Token must be of type Refresh or MobileRefresh. Provided token has type: {token.Type}", $"Token must be of type Refresh or MobileRefresh. Provided token has type: {token.Type}",
nameof(token)); nameof(token));
var refreshToken = token.SignedToken; SetSteamRefreshTokenUnsafe(container, token, SteamLoginUri);
container.Add(SteamLoginUri, new Cookie(REFRESH_COOKIE_NAME, refreshToken) }
/// <summary>
/// Sets a Steam refresh token as a cookie to the specified cookie container for the given domain.
/// </summary>
/// <remarks>
/// This method does not perform validation on the input parameters. Callers must ensure that the
/// provided values are valid and appropriate for use.
/// </remarks>
/// <param name="container">The cookie container to which the refresh token cookie will be added. Cannot be null.</param>
/// <param name="token">
/// The Steam authentication token containing the signed token value and expiration information. Cannot
/// be null.
/// </param>
/// <param name="domainUri">The URI of the domain for which the refresh token cookie should be set. Cannot be null.</param>
public static void SetSteamRefreshTokenUnsafe(CookieContainer container, SteamAuthToken token, Uri domainUri)
{
container.Add(domainUri, new Cookie(REFRESH_COOKIE_NAME, token.SignedToken)
{ {
Expires = token.Expires.ToLocalDateTime() Expires = token.Expires.ToLocalDateTime()
}); });
} }
public static void AddTokenCookie(CookieContainer container, SteamAuthToken token) /// <summary>
/// Sets a Steam access token to the specified cookie container for use with Steam web requests.
/// </summary>
/// <remarks>
/// This method is intended for standard web access tokens bound to a specific Steam web domain.
/// Mobile tokens are intentionally rejected, since their audiences are capability-based rather
/// than domain-based and may grant access to multiple web domains.
/// </remarks>
/// <param name="container">The cookie container to which the Steam access token will be added. Cannot be null.</param>
/// <param name="token">
/// The Steam access token to add. Must be of type AccessToken and associated with a valid Steam
/// domain.
/// </param>
/// <exception cref="ArgumentException">Thrown if the token is not of type AccessToken.</exception>
public static void SetSteamAccessToken(this CookieContainer container, SteamAuthToken token)
{ {
if (token.Type == SteamAccessTokenType.Mobile)
throw new ArgumentException(
$"Mobile access tokens cannot be added using this method. Use SetSteamAccessTokenUnsafe instead. Provided token has type: {token.Type}",
nameof(token));
if (token.Type is not SteamAccessTokenType.AccessToken) if (token.Type is not SteamAccessTokenType.AccessToken)
throw new ArgumentException($"Token must be of type AccessToken. Provided token has type: {token.Type}", throw new ArgumentException($"Token must be of type AccessToken. Provided token has type: {token.Type}",
nameof(token)); nameof(token));
var domain = SteamDomains.GetDomainUri(token.Domain); var domainUri = SteamDomains.GetDomainUri(token.Domain);
container.Add(domain, new Cookie(ACCESS_COOKIE_NAME, token.SignedToken) container.SetSteamAccessTokenUnsafe(token, domainUri);
}
/// <summary>
/// Sets a Steam access token as a secure, HTTP-only cookie to the specified cookie container for the given domain.
/// </summary>
/// <remarks>
/// This method does not perform validation on the input parameters and should only be used when
/// input values are trusted. The added cookie is marked as secure and HTTP-only, and its expiration is set
/// according to the token's expiration time.
/// </remarks>
/// <param name="container">The cookie container to which the Steam access token cookie will be added. Cannot be null.</param>
/// <param name="token">The Steam access token to add as a cookie. Must contain a valid signed token and expiration.</param>
/// <param name="domainUri">The URI of the domain for which the cookie will be set. Cannot be null.</param>
public static void SetSteamAccessTokenUnsafe(this CookieContainer container, SteamAuthToken token, Uri domainUri)
{
container.Add(domainUri, new Cookie(ACCESS_COOKIE_NAME, token.SignedToken)
{ {
HttpOnly = true, HttpOnly = true,
Secure = true, Secure = true,
@@ -257,7 +262,6 @@ public static class AdmissionHelper
}); });
} }
public static bool IsSteamCookie(Cookie cookie) public static bool IsSteamCookie(Cookie cookie)
{ {
return cookie.Domain.Contains("steamcommunity.com") || cookie.Domain.Contains("steampowered.com") || return cookie.Domain.Contains("steamcommunity.com") || cookie.Domain.Contains("steampowered.com") ||
+20 -26
View File
@@ -1,12 +1,11 @@
using System.Collections.ObjectModel; using SteamLibForked.Models.Core;
using SteamLibForked.Models.Core; using System.Collections.Immutable;
namespace SteamLib.Core; namespace SteamLib.Core;
public static class SteamDomains public static class SteamDomains
{ {
public static IReadOnlyDictionary<SteamDomain, string> Domains { get; } = public static IReadOnlyDictionary<SteamDomain, string> Domains { get; } =
new ReadOnlyDictionary<SteamDomain, string>(
new Dictionary<SteamDomain, string> new Dictionary<SteamDomain, string>
{ {
{SteamDomain.Community, SteamConstants.STEAM_COMMUNITY}, {SteamDomain.Community, SteamConstants.STEAM_COMMUNITY},
@@ -16,34 +15,29 @@ public static class SteamDomains
{SteamDomain.Checkout, SteamConstants.STEAM_CHECKOUT}, {SteamDomain.Checkout, SteamConstants.STEAM_CHECKOUT},
{SteamDomain.Login, SteamConstants.STEAM_LOGIN}, {SteamDomain.Login, SteamConstants.STEAM_LOGIN},
{SteamDomain.API, SteamConstants.STEAM_API} {SteamDomain.API, SteamConstants.STEAM_API}
}); }.ToImmutableDictionary();
public static IReadOnlyDictionary<SteamDomain, Uri> DomainUris { get; } public static IReadOnlyDictionary<SteamDomain, Uri> DomainUris { get; }
= new ReadOnlyDictionary<SteamDomain, Uri>( = Domains
Domains.ToDictionary(x => x.Key, x => new Uri(x.Value)) .ToDictionary(x => x.Key, x => new Uri(x.Value))
); .ToImmutableDictionary();
public static IEnumerable<SteamDomain> AllDomains { get; } = /// <summary>
[ /// All known public Steam domains.
SteamDomain.Community, /// </summary>
SteamDomain.Store, public static IEnumerable<SteamDomain> AllDomains { get; } = ImmutableHashSet.Create(SteamDomain.Community,
SteamDomain.Help, SteamDomain.Store, SteamDomain.Help, SteamDomain.TV, SteamDomain.Checkout, SteamDomain.Login, SteamDomain.API);
SteamDomain.TV,
SteamDomain.Checkout,
SteamDomain.Login,
SteamDomain.API
];
public static IEnumerable<SteamDomain> AuthDomains { get; } =
[
SteamDomain.Community,
SteamDomain.Store,
SteamDomain.Help,
SteamDomain.TV,
SteamDomain.Checkout
];
/// <summary>
/// Steam web domains that participate in the standard login authorization flow.
/// <para>
/// These domains use the <c>web:*</c> audience format and receive
/// authentication cookies/tokens during session initialization.
/// </para>
/// </summary>
public static IReadOnlySet<SteamDomain> WebDomains { get; } = ImmutableHashSet.Create(SteamDomain.Community,
SteamDomain.Store, SteamDomain.Help, SteamDomain.TV, SteamDomain.Checkout);
public static Uri GetDomainUri(SteamDomain domain) public static Uri GetDomainUri(SteamDomain domain)
{ {
@@ -1,13 +1,14 @@
using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json;
using Newtonsoft.Json; using SteamLib.Core;
using SteamLibForked.Abstractions; using SteamLibForked.Abstractions;
using SteamLibForked.Models.Core; using SteamLibForked.Models.Core;
using System.Diagnostics.CodeAnalysis;
namespace SteamLibForked.Models.Session; namespace SteamLibForked.Models.Session;
//WARNING: Any changes here should be reflected in MafileSerializer.cs //WARNING: Any changes here should be reflected in MafileSerializer.cs
public sealed class MobileSessionData : SessionData, IMobileSessionData public class MobileSessionData : SessionData, IMobileSessionData
{ {
public SteamAuthToken? MobileToken { get; private set; } public SteamAuthToken? MobileToken { get; private set; }
@@ -31,9 +32,22 @@ public sealed class MobileSessionData : SessionData, IMobileSessionData
return MobileToken; return MobileToken;
} }
public override SteamAuthToken? GetToken(SteamDomain domain)
{
var isWeb = SteamDomains.WebDomains.Contains(domain);
if (isWeb)
{
// Mobile-issued tokens usually contain the "web" audience,
// so we assume they can also be used for all web:* domains.
// See: SteamAuthToken 'TODO' for more details
return MobileToken ?? base.GetToken(domain);
}
return base.GetToken(domain);
}
[MemberNotNull(nameof(MobileToken))] [MemberNotNull(nameof(MobileToken))]
public void SetMobileToken(SteamAuthToken token) public virtual void SetMobileToken(SteamAuthToken token)
{ {
if (token.Type != SteamAccessTokenType.Mobile) if (token.Type != SteamAccessTokenType.Mobile)
throw new ArgumentException("Token must be of type MobileAccess", nameof(token)) throw new ArgumentException("Token must be of type MobileAccess", nameof(token))
@@ -44,13 +58,13 @@ public sealed class MobileSessionData : SessionData, IMobileSessionData
MobileToken = token; MobileToken = token;
} }
public override MobileSessionData Clone()
{
return (MobileSessionData) ((ISessionData) this).Clone();
}
object ICloneable.Clone() object ICloneable.Clone()
{ {
return new MobileSessionData(SessionId, SteamId, RefreshToken, MobileToken, Tokens); return new MobileSessionData(SessionId, SteamId, RefreshToken, MobileToken, Tokens);
} }
public override MobileSessionData Clone()
{
return (MobileSessionData)((ISessionData)this).Clone();
}
} }
+1 -1
View File
@@ -31,7 +31,7 @@ public static class ClientBuilder
} }
else else
{ {
container.SetSteamMobileCookiesWithMobileToken(sessionData); container.SetSteamMobileCookies(sessionData);
} }
//Nebula tweak: //Nebula tweak: