openiddict / openiddict-core

Flexible and versatile OAuth 2.0/OpenID Connect stack for .NET
https://openiddict.com/
Apache License 2.0
4.47k stars 528 forks source link

Xbox authentication #1763

Closed verdie-g closed 1 year ago

verdie-g commented 1 year ago

Confirm you've already contributed to this project or that you sponsor it

Version

4.x

Describe the bug

When trying to use the XboxLive.profile scope I'm getting the following error:

{
    "error": "invalid_scope", 
    "error_description": "The provided value for the input parameter 'scope' is not valid. The scope 'openid XboxLive.profile' does not exist.",
    "state": "4MR0QXicSbQ5H9-O_KLePIl8urKpnrAkR4utt4O04SA"
}

Since openid is prepended to the scope name, it looks like there is a formatting issue.

To reproduce

options.UseWebProviders().UseMicrosoft(microsoft =>
{
    microsoft.SetClientId(microsoftClientId)
        .SetClientSecret(microsoftClientSecret)
        .SetRedirectUri("connect/callback-microsoft")
        .Configure(microsoft => microsoft.Scopes.Add("XboxLive.profile"));
});

Exceptions (if any)

No response

verdie-g commented 1 year ago

I think I used the wrong tenant. I'm trying to confirm that.

kevinchalet commented 1 year ago

Are you sure the XboxLive.profile exists and is supported by Azure AD?

verdie-g commented 1 year ago

I can't find any relevant doc regarding xbox login...

verdie-g commented 1 year ago

I found https://login.live.com/.well-known/openid-configuration. According to https://sourcegraph.com/search?q=https%3A%2F%2Flogin.live.com%2Foauth20_authorize.srf it looks like it's what is used for xbox login.

verdie-g commented 1 year ago

I'm struggling to find the differences between login.microsoftonline.com and login.live.com. Here is what chat-gpt can tell me:

https://login.microsoftonline.com/ is the OIDC provider for Azure Active Directory (Azure AD), which is a cloud-based identity and access management service provided by Microsoft. Azure AD provides authentication and authorization for applications and services, including Microsoft Office 365, Azure, and many others. Azure AD supports enterprise-level authentication scenarios and provides features such as multifactor authentication, conditional access policies, and single sign-on across multiple applications.

On the other hand, https://login.live.com/ is the OIDC provider for Microsoft accounts. Microsoft accounts are used by individuals to access various Microsoft services, such as Outlook.com, OneDrive, Xbox, and others. Microsoft accounts provide basic authentication features, such as multifactor authentication and password reset options.

Should a new provider be added for login.live.com @kevinchalet ?

kevinchalet commented 1 year ago

lol, I wouldn't believe a single word of what ChatGPT says 🤣 (the common tenant redirects you to https://login.live.com/ at some point anyway)

Try adding Xboxlive.signin to the list of requested scopes. It seems to work on my machine.

verdie-g commented 1 year ago

I was able to reach the xbox consent page again but I get the initial error

{
    "error": "invalid_scope",
    "error_description": "AADSTS70011: The provided value for the input parameter 'scope' is not valid. The scope '' is not configured for this tenant.\r\nTrace ID: c1289c17-1843-406c-b9e8-e7820059fa00\r\nCorrelation ID: 0c689748-7774-4c9e-8249-d2bcb33b12eb\r\nTimestamp: 2023-05-06 12:27:31Z",
    "error_codes": [     70011   ],
    "timestamp": "2023-05-06 12:27:31Z",
    "trace_id": "c1289c17-1843-406c-b9e8-e7820059fa00",
    "correlation_id": "0c689748-7774-4c9e-8249-d2bcb33b12eb"
}

Could you confirm you are also getting that one?

kevinchalet commented 1 year ago

Hum yeah, but only if I specify the Xboxlive.signin scope with other scopes like User.Read or profile. If I use the Xboxlive.signin scope alone, I get a different error, returned by the userinfo endpoint:

{
  "error": {
    "code": "InvalidAuthenticationToken",
    "message": "CompactToken parsing failed with error code: 8004920A",
    "innerError": {
      "date": "2023-05-06T12:57:02",
      "request-id": "8e3b0e6c-00d0-4bff-91e7-042ba2e4428f",
      "client-request-id": "8e3b0e6c-00d0-4bff-91e7-042ba2e4428f"
    }
  }
}

It's possible (and even probable) that the Xbox service expects a very specific type of access token that is not returned when you ask an access token with a scope pointing to different Graph API/Xbox services.

You can disable userinfo retrieval using a custom handler, like this one:

/// <summary>
/// Contains the logic responsible for disabling the userinfo retrieval for the providers that require it.
/// </summary>
public sealed class DisableUserinfoRetrieval : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
    /// <summary>
    /// Gets the default descriptor definition assigned to this handler.
    /// </summary>
    public static OpenIddictClientHandlerDescriptor Descriptor { get; }
        = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
            .UseSingletonHandler<DisableUserinfoRetrieval>()
            .SetOrder(EvaluateUserinfoRequest.Descriptor.Order + 250)
            .SetType(OpenIddictClientHandlerType.BuiltIn)
            .Build();

    /// <inheritdoc/>
    public ValueTask HandleAsync(ProcessAuthenticationContext context)
    {
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        context.SendUserinfoRequest = context.Registration.ProviderName switch
        {
            Providers.Microsoft when
                context.GrantType is GrantTypes.AuthorizationCode or GrantTypes.Implicit &&
                context.StateTokenPrincipal is ClaimsPrincipal principal &&
                principal.GetScopes().Any(scope => scope.StartsWith("Xboxlive.", StringComparison.OrdinalIgnoreCase))
                => false,

            Providers.Microsoft when context.GrantType is GrantTypes.RefreshToken &&
                context.Scopes.Any(scope => scope.StartsWith("Xboxlive.", StringComparison.OrdinalIgnoreCase))
                => false,

            _ => context.SendUserinfoRequest
        };

        return default;
    }
}

... but then, you don't get many claims in the resulting principal. Maybe you're expected to call some Xbox API to get more information about the player's profile?

I guess we should ping someone working at Microsoft to better understand how this mess works 🤣

verdie-g commented 1 year ago

According to https://stackoverflow.com/questions/74157060/microsoft-graph-compacttoken-parsing-failed-with-error-code-8004920a we could get that error if we are mixing scopes from different namespaces. Which scopes is openiddict passing by default?

Also, what do you think of adding debug logs for all HTTP requests so I can see the query parameters sent to identification server.

verdie-g commented 1 year ago

Also I've tried setting up a openiddict client using https://login.live.com/.well-known/openid-configuration but it doesn't like the jwks_uri:

An unsupported OK response was returned by the remote HTTP server: text/json; charset=utf-8 {"keys":[{"e":"AQAB","kid":"1LTMzakihiRla_8z2BEJVXeWMqo","kty":"RSA","n":"3sKc
JSD4cHwTY5jYm5lNEzqk3wON1CaARO5EoWIQt5u-X-ZnW61CiRZpWpfhKwRYU153td5R8p-AJDWT-NcEJ0MHU3KiuIEPmbgJpS7qkyURuHRucDM2lO4L4XfIlvizQrlyJnJcd09uLErZEO9PcvKiDHoois2B4fGj7CsAe5UZgExJvACD
lsQSku2JUyDmZUZP2_u_gCuqNJM5o0hW7FKRI3MFoYCsqSEmHnnumuJ2jF0RHDRWQpodhlAR6uKLoiWHqHO3aG7scxYMj5cMzkpe1Kq_Dm5yyHkMCSJ_JaRhwymFfV_SWkqd3n-WVZT0ADLEq0RNi9tqZ43noUnO_w","use":"sig",
"x5c":["MIIDYDCCAkigAwIBAgIJAIB4jVVJ3BeuMA0GCSqGSIb3DQEBCwUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleTAeFw0xNjA0MDUxNDQzMzVaFw0yMTA0MDQxNDQzMzVaMCkxJzAlBgNVBAMT
HkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN7CnCUg+HB8E2OY2JuZTRM6pN8DjdQmgETuRKFiELebvl/mZ1utQokWaVqX4SsEWFNed7XeUfKfgCQ1k/jXBCdDB1Ny
oriBD5m4CaUu6pMlEbh0bnAzNpTuC+F3yJb4s0K5ciZyXHdPbixK2RDvT3Lyogx6KIrNgeHxo+wrAHuVGYBMSbwAg5bEEpLtiVMg5mVGT9v7v4ArqjSTOaNIVuxSkSNzBaGArKkhJh557pridoxdERw0VkKaHYZQEerii6Ilh6hzt2hu
7HMWDI+XDM5KXtSqvw5ucsh5DAkifyWkYcMphX1f0lpKnd5/llWU9AAyxKtETYvbameN56FJzv8CAwEAAaOBijCBhzAdBgNVHQ4EFgQU9IdLLpbC2S8Wn1MCXsdtFac9SRYwWQYDVR0jBFIwUIAU9IdLLpbC2S8Wn1MCXsdtFac9SRah
LaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAIB4jVVJ3BeuMAsGA1UdDwQEAwIBxjANBgkqhkiG9w0BAQsFAAOCAQEAXk0sQAib0PGqvwELTlflQEKS++vqpWYPW/2gCVCn5shbyP1J7z1nT8kE
/ZDVdl3LvGgTMfdDHaRF5ie5NjkTHmVOKbbHaWpTwUFbYAFBJGnx+s/9XSdmNmW9GlUjdpd6lCZxsI6888r0ptBgKINRRrkwMlq3jD1U0kv4JlsIhafUIOqGi4+hIDXBlY0F/HJPfUU75N885/r4CCxKhmfh3PBM35XOch/NGC67fLjq
LN+TIWLoxnvil9m3jRjqOA9u50JUeDGZABIYIMcAdLpI2lcfru4wXcYXuQul22nAR7yOyGKNOKULoOTE4t4AeGRqCogXSxZgaTgKSBhvhE+MGg=="],"x5t":"1LTMzakihiRla_8z2BEJVXeWMqo"},{"kty":"RSA","use":"sig"
 ,"kid":"bW8ZcMjBCnJZS-ibX5UQDNStvx4","x5t":"bW8ZcMjBCnJZS-ibX5UQDNStvx4","n":"2a70SwgqIh8U-Shj_VJJGBheEVk2F4ygmMCRtKUAb1jMP6R1j5Mc5xaqhgzlWjckJI1lx4rha1oNLrdg8tJBxdm8V8xZohCOa
nJ52uAwoc6FFTY3VRLaUZSJ3zCXfuJwy4KvFHJUAuLhLj0hVeq-y10CmRJ1_MPTuNRJLdblSWcXyWYIikIRggQWS04M-QjR7571mX-Lu_eDs8xJVrnNFMVGRmFqf3EFD4QLNjW9JJj0m_prnTv41V_E8AA7MQZ12ip3u5aeOAQqGjVyz
dHxvV9laxta6XWaM8QSTIu_Zav1-aDYExp99nCP4Hw0_Oom5vK5N88DB8VM0mouQi8a8Q","e":"AQAB","x5c":["MIIDYDCCAkigAwIBAgIJAN2X7t+ckntxMA0GCSqGSIb3DQEBCwUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIF
NpZ25pbmcgUHVibGljIEtleTAeFw0yMTAzMjkyMzM4MzNaFw0yNjAzMjgyMzM4MzNaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANmu9EsIKi
IfFPkoY/1SSRgYXhFZNheMoJjAkbSlAG9YzD+kdY+THOcWqoYM5Vo3JCSNZceK4WtaDS63YPLSQcXZvFfMWaIQjmpyedrgMKHOhRU2N1US2lGUid8wl37icMuCrxRyVALi4S49IVXqvstdApkSdfzD07jUSS3W5UlnF8lmCIpCEYIEFk
tODPkI0e+e9Zl/i7v3g7PMSVa5zRTFRkZhan9xBQ+ECzY1vSSY9Jv6a507+NVfxPAAOzEGddoqd7uWnjgEKho1cs3R8b1fZWsbWul1mjPEEkyLv2Wr9fmg2BMaffZwj+B8NPzqJubyuTfPAwfFTNJqLkIvGvECAwEAAaOBijCBhzAdBg
NVHQ4EFgQU57BsETnF8TctGU87R4N9YxmNWoIwWQYDVR0jBFIwUIAU57BsETnF8TctGU87R4N9YxmNWoKhLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAN2X7t+ckntxMAsGA1UdDwQEAwIBxj
ANBgkqhkiG9w0BAQsFAAOCAQEAcsk+LGlTzSQdnh3mtCBMNCGZCiTYvFcqenwjDf1/c4U+Yi7fxYmAXm7wVLX+GVMxpLPpzMuVOXztGoPMUgWH59CFWhsMvZbIUKsd8xbEKmls1ZIgxRYdagcWTGeBET6XIoF6Ba57BhRCxFPslhIpg2
7/HnfHtTdGfjRpafNbBYvC/9PL/s2E9U4AklpUn2W19UiJLRFgXGPjYPLW0j1Od0qzHHJ84saclVwvuOrpp75Y+0Du5Z2OrjNF1W4dEWZMJmmOe73ejAnoiWJI25kQpkd4ooNasw3HIZEJZ6cKctmPJLdvx0tJ8bde4DivtWOeFIwcAk
okH2jlHmAOipNETw=="]}]}.

Maybe that could be related?

kevinchalet commented 1 year ago

Which scopes is openiddict passing by default?

Only openid, if the server supports OIDC.

Also, what do you think of adding debug logs for all HTTP requests so I can see the query parameters sent to identification server.

Yeah, adding more logs to the client stack is on my radar :smile:

Also I've tried setting up a openiddict client using https://login.live.com/.well-known/openid-configuration but it doesn't like the jwks_uri: Maybe that could be related?

No, that OIDC server implementation simply doesn't return a valid content type: since text/json is not a standard media type for JSON, OpenIddict doesn't try to extract it and returns an error.

verdie-g commented 1 year ago

To summarize the different errors

Common tenant:

Consumers tenant:

It seems like I need to use the consumers tenant but as you said the userinfo endpoint won't work. Reading https://wiki.vg/Microsoft_Authentication_Scheme, https://gitlab.bixilon.de/bixilon/minosoft/-/blob/master/src/main/java/de/bixilon/minosoft/util/account/microsoft/MicrosoftOAuthUtils.kt and https://gist.github.com/tuxuser/8b7cc153cdecd0a9c3f2694850fa90bd it seems like I need to use the access token to authenticate the user to xbox on https://user.auth.xboxlive.com/user/authenticate.

I'll try that.

kevinchalet commented 1 year ago

To summarize the different errors

Sounds like a good summary (it's honestly a real mess and some of the error messages are completely inaccurate and misleading...)

Let me know once you have a chance to test the /user/authenticate API.

I'm considering adding the handler I shared earlier as a way to disable userinfo retrieval when we detect scopes that prevent that are used, but I'm all ears if you have a different approach in mind (doing nothing is an option too 😄)

verdie-g commented 1 year ago

Here is the complete code to get the xbox profile

private static readonly Uri XasuEndpoint = new("https://user.auth.xboxlive.com/user/authenticate");
private static readonly Uri XstsEndpoint = new("https://xsts.auth.xboxlive.com/xsts/authorize");
private static readonly Uri XboxProfileEndpoint = new("https://profile.xboxlive.com/users/me/profile/settings");
private static readonly HttpClient HttpClient = new();
private static readonly JsonSerializerOptions CamelCaseJsonSerializerOptions = new()
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

var xasuTokenResponse = await GetTokenAsync(XasuEndpoint, new XasTokenRequest(
    "JWT",
    "http://auth.xboxlive.com",
    new Dictionary<string, object>
    {
        ["AuthMethod"] = "RPS",
        ["SiteName"] = "user.auth.xboxlive.com",
        ["RpsTicket"] = $"d={context.BackchannelAccessToken}",
    }));

var xstsTokenResponse = await GetTokenAsync(XstsEndpoint, new XasTokenRequest(
    "JWT",
    "http://xboxlive.com",
    new Dictionary<string, object>
    {
        ["UserTokens"] = new[] { xasuTokenResponse.Token },
        ["SandboxId"] = "RETAIL",
    }));

string userHash = xstsTokenResponse.DisplayClaims["xui"][0]["uhs"];
var xboxProfile = await GetXboxProfileAsync(userHash, xstsTokenResponse.Token);
// To get the avatar picture https://sourcegraph.com/github.com/TimScriptov/ModdedPE@6d7c128/-/blob/xbox/src/main/java/com/microsoft/xbox/xle/app/ImageUtil.java

private async Task<XasTokenResponse> GetTokenAsync(Uri uri, XasTokenRequest tokenRequest)
{
    string tokenRequestJson = JsonSerializer.Serialize(tokenRequest);

    var res = await HttpClient.PostAsync(uri,
        new StringContent(tokenRequestJson, Encoding.UTF8, MediaTypeNames.Application.Json));
    res.EnsureSuccessStatusCode();

    string tokenResponseJson = await res.Content.ReadAsStringAsync();
    return JsonSerializer.Deserialize<XasTokenResponse>(tokenResponseJson)!;
}

private async Task<XboxProfile> GetXboxProfileAsync(string userHash, string xstsToken)
{
    string authorization = $"XBL3.0 x={userHash};{xstsToken}";

    UriBuilder uriBuilder = new(XboxProfileEndpoint) { Query = "settings=GamerTag,GameDisplayName,GameDisplayPicRaw" };
    var res = await HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, uriBuilder.Uri)
    {
        Headers =
        {
            { "Authorization", authorization },
            { "x-xbl-contract-version", "3" },
        },
    });

    string xboxProfileResponseJson = await res.Content.ReadAsStringAsync();
    var xboxProfileResponse = JsonSerializer.Deserialize<XboxProfileResponse>(xboxProfileResponseJson, CamelCaseJsonSerializerOptions)!;
    return xboxProfileResponse.ProfileUsers[0];
}

private record XasTokenRequest(string TokenType, string RelyingParty, Dictionary<string, object> Properties);
private record XasTokenResponse(DateTime IssueInstant, DateTime NotAfter, string Token,
    Dictionary<string, Dictionary<string, string>[]> DisplayClaims);
private record XboxProfileResponse(XboxProfile[] ProfileUsers, bool IsSponsoredUser);
private record XboxProfile(string Id, string HostId, IdValuePair[] Settings);
private record IdValuePair(string Id, string Value);

References:

kevinchalet commented 1 year ago

I opened https://github.com/openiddict/openiddict-core/pull/1770 to add the handler I mentioned earlier.

verdie-g commented 1 year ago

Thanks! Could you give me some pointers to make my own userinfo handler for when a xbox scope is specified 🙏

kevinchalet commented 1 year ago

Thanks! Could you give me some pointers to make my own userinfo handler for when a xbox scope is specified 🙏

Here's an example of a handler that resolves the userinfo claims from a different place (here, from custom parameters returned as part of the token response, instead of a dedicated endpoint):

https://github.com/openiddict/openiddict-core/blob/c2f12183cde4b46475d56e94ffa374b42e040f45/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs#L583-L677

That said, I'd personally opt for a different approach by doing the Xbox profile resolution directly in your /callback/login/microsoft callback MVC action.