OpenXbox / xbox-webapi-python

A python library to authenticate with Xbox Live via your Microsoft Account and provides Xbox related Web-API.
https://pypi.python.org/pypi/xbox-webapi
MIT License
175 stars 44 forks source link

400 Bad Request when trying to authenticate #45

Closed pacMakaveli closed 3 years ago

pacMakaveli commented 3 years ago

Hi, does this method still work? I seem to hit 400 Bad Request whenever I try to authenticate the user. I know the authenticate logic works because it works fine when a token is returned using the username/password flow, just not with oauth.

Is this related to?

Starting November 9th, 2020 end users will no longer be able to grant consent to newly registered multitenant apps without verified publishers.

Code if needed: It's ruby, but the flow is the same, just trying to port it for my ruby app.


      def oauth2_token_request()
        client_id = ''
        client_secret = ''

        params = {
          client_id: client_id,
          response_type: 'code',
          approval_prompt: 'auto',
          scope: DEFAULT_SCOPES.join(' '),
          redirect_uri: 'http://localhost:3000/my_auth/xbox'
        }

        url = "https://login.live.com/oauth20_authorize.srf?#{ URI.encode_www_form(params) }"

        puts url
        puts 'Please input your code: '
        code = gets.strip

        request = self.class.post("https://login.live.com/oauth20_token.srf",
          body: {
            grant_type: 'authorization_code',
            code: code,
            client_id: client_id,
            client_secret: client_secret,
            scope: DEFAULT_SCOPES.join(' '),
            redirect_uri: 'http://localhost:3000/my_auth/xbox'
          }
        ).parsed_response

        user_auth = {
          token_type: request['token_type'],
          expires_in: (Time.now + (request['expires_in'] || 0)),
          scope: request['scope'],
          access_token: request['access_token'],
          refresh_token: request['refresh_token'],
          user_id: request['user_id'],
          issued_at: DateTime.now.to_s
        }
        puts request
        authenticate(request['access_token'])
      end

      def authenticate(access_token)
        body = {
          'RelyingParty' => 'http://auth.xboxlive.com',
          'TokenType' => 'JWT',
          'Properties' => {
            'AuthMethod' => 'RPS',
            'SiteName' => 'user.auth.xboxlive.com',
            'RpsTicket' => "d=#{ access_token }"
          }
        }
        headers = {
          'x-xbl-contract-version' => '1'
        }

        url = 'https://user.auth.xboxlive.com/user/authenticate'
        request = self.class.post(url, body: body, headers: headers, debug_output: $stdout)

        binding.pry
        token = request['Token']
        uhs = request.dig('DisplayClaims', 'xui', 0, 'uhs')

        return token, uhs
      end

I've already tried: https://gist.github.com/tuxuser/8b7cc153cdecd0a9c3f2694850fa90bd with no luck.

Here's the request:

https://login.live.com/oauth20_authorize.srf?client_id=[]&response_type=code&approval_prompt=auto&scope=Xboxlive.signin+Xboxlive.offline_access&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fmy_auth%2Fxbox
Please input your code:
  M.R3_BAY.e034920f-6dc5-5bc3-1852-0a2919f8f30b
{"token_type"=>"bearer", "expires_in"=>3600, "scope"=>"XboxLive.signin XboxLive.offline_access", "access_token"=>"EwA4A+pvBAAUKods63Ys1fGlwiccIFJ+qE1hANsAAbiIYjDJky2QL6/X8P7OGIRcd05TeP/im5rzQYoxgWeMpL5SoB73O2uE73b2O3WrPx6Jf2Bbtur8YiZwncnlzFkSIn9atIPWYyGNYLWBDW+QMUQ0sguxVfJv+TOo2UVxeEy4zYI3l+ytp1yGgVeki3SnOtTs9t7yYvMd/euHQOfZNy8JniOkyRDd5VFpg8bRNmPbInLAcyKWE0xyuLeR04hFResTxDb+GEqRRtBIXZ+AoqhdTZnJQxYDIDnYmtwdjMUpO5yA/P4KJeKMwyr3m8HtG0qz8A9ZTFrMGUfSrhBQx9kc5EhZwGMYsScSe2XmYZ9jPTrx/5TQHxnGdPFwjmUDZgAACEK4c+VYuIKBCAJyLPFlAstkoFupkGIZvHRKYjq7x3gYBTdoUoF0GauH5D2o44ri8XZriAT8emvneWiqPQwS67mCD3e8lWgoJ1IiQ0hzwybRvRbeKvtQyMx4M6l+AWqqLnJleIQkPw8BPKWALBwTlok5o8RXPcc9fbW0KBJ6gZuzzvRGTSXUCVsqC1mRA5rXRTPJmbl7M2p8Up2d/WMYGWbr82kx8NeLKc6aftPWpyK/6blAH1bAWySvrL1kHjWPjC/7P0VGSqVW8NdzYOn2qBo2EXt6ZhYeCa6EW21r8mXy8zt8WTlNIKWcumI+D3yh+eglFmfkTwtl7ZqZPBoAHatXFk1XeQ3ab4OPAdrpgbUvalxe2eA+sexYMnld5SzlfYCWVow6mzIHCl1cc2LxpaEpFZqgbnw8Y+bM1zBx1kUZtiJQSRZ/6U6TTzvl5LEh0wmhPm6RewiKrae9NRFebO1+3vh4my/SunzbYXkW6MzH1TCGceGY8QWIXpLutoLSoVr6MlM7ROaIaVe73jWM9w7YL3tsd6i9doDk+oofuQ8TK+w9QQgQ5cHaW+HJ+HV784R8a6dEiLTRmzbRZdbeUIpwozea/hiBDuhysRWBh6M3oLvHXDCvj2igAblOdyYRS7pfPKAb2nng9ViX5BWQqLcunRg8k9EaQqyB84WYlWhNvcvGMTHBB0L5XyimZ8NKaDy+LQI=", "refresh_token"=>"M.R3_BAY.CayW3LvCUOqPgkjJFVyGtQ534sq11ocTJaYWEf3nacddh14dHhtrrzOTiizFQaVQhli6hfZ6Qdq5hgHm04cFcFvvOahxpNznRCu9artf6EE1*s86KDv0kT9tSg0WhkV8pRmPgmvUjemNDPqxueZEfqifMoQZOcwy9se46sG4nX2uK3d4HJo1lA4jCeXTMXV5mT3uuL!YFLHWOLyB1k7CZKvjNUrlaEA0s0lc7xVoSrSfSHOo9331s9s84mWVrioDmoQa8KdPVc1eiYnc!BFz8J5SZq!T1Kn3dVYRFj7wkIDLILx!MbsjaSEMvwbXO3BfvRo4ViOBXCkXX!jaQ9j6a2plEg7fMFMga2Nl27IazvcKlod1oRMS78zaTXPMx2ilInaTZV7354*qpKiQqXwDVJ4$", "user_id"=>""}
opening connection to user.auth.xboxlive.com:443...
  opened
starting SSL for user.auth.xboxlive.com:443...
  SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384
<- "POST /user/authenticate HTTP/1.1\r\nX-Xbl-Contract-Version: 1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: user.auth.xboxlive.com\r\nContent-Length: 1329\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\n"
<- "RelyingParty=http%3A%2F%2Fauth.xboxlive.com&TokenType=JWT&Properties%5BAuthMethod%5D=RPS&Properties%5BSiteName%5D=user.auth.xboxlive.com&Properties%5BRpsTicket%5D=d%3DEwA4A%2BpvBAAUKods63Ys1fGlwiccIFJ%2BqE1hANsAAbiIYjDJky2QL6%2FX8P7OGIRcd05TeP%2Fim5rzQYoxgWeMpL5SoB73O2uE73b2O3WrPx6Jf2Bbtur8YiZwncnlzFkSIn9atIPWYyGNYLWBDW%2BQMUQ0sguxVfJv%2BTOo2UVxeEy4zYI3l%2Bytp1yGgVeki3SnOtTs9t7yYvMd%2FeuHQOfZNy8JniOkyRDd5VFpg8bRNmPbInLAcyKWE0xyuLeR04hFResTxDb%2BGEqRRtBIXZ%2BAoqhdTZnJQxYDIDnYmtwdjMUpO5yA%2FP4KJeKMwyr3m8HtG0qz8A9ZTFrMGUfSrhBQx9kc5EhZwGMYsScSe2XmYZ9jPTrx%2F5TQHxnGdPFwjmUDZgAACEK4c%2BVYuIKBCAJyLPFlAstkoFupkGIZvHRKYjq7x3gYBTdoUoF0GauH5D2o44ri8XZriAT8emvneWiqPQwS67mCD3e8lWgoJ1IiQ0hzwybRvRbeKvtQyMx4M6l%2BAWqqLnJleIQkPw8BPKWALBwTlok5o8RXPcc9fbW0KBJ6gZuzzvRGTSXUCVsqC1mRA5rXRTPJmbl7M2p8Up2d%2FWMYGWbr82kx8NeLKc6aftPWpyK%2F6blAH1bAWySvrL1kHjWPjC%2F7P0VGSqVW8NdzYOn2qBo2EXt6ZhYeCa6EW21r8mXy8zt8WTlNIKWcumI%2BD3yh%2BeglFmfkTwtl7ZqZPBoAHatXFk1XeQ3ab4OPAdrpgbUvalxe2eA%2BsexYMnld5SzlfYCWVow6mzIHCl1cc2LxpaEpFZqgbnw8Y%2BbM1zBx1kUZtiJQSRZ%2F6U6TTzvl5LEh0wmhPm6RewiKrae9NRFebO1%2B3vh4my%2FSunzbYXkW6MzH1TCGceGY8QWIXpLutoLSoVr6MlM7ROaIaVe73jWM9w7YL3tsd6i9doDk%2BoofuQ8TK%2Bw9QQgQ5cHaW%2BHJ%2BHV784R8a6dEiLTRmzbRZdbeUIpwozea%2FhiBDuhysRWBh6M3oLvHXDCvj2igAblOdyYRS7pfPKAb2nng9ViX5BWQqLcunRg8k9EaQqyB84WYlWhNvcvGMTHBB0L5XyimZ8NKaDy%2BLQI%3D"
-> "HTTP/1.1 400 Bad Request\r\n"
-> "Cache-Control: no-cache, no-store\r\n"
-> "Content-Length: 0\r\n"
-> "MS-CV: G45KgNf/vEiFoy4qfpYJFA.0\r\n"
-> "X-Content-Type-Options: nosniff\r\n"
-> "X-XblCorrelationId: 00000000-0000-0000-0000-000000000000\r\n"
-> "Date: Thu, 14 Jan 2021 10:08:09 GMT\r\n"
-> "Connection: close\r\n"
-> "\r\n"
reading 0 bytes...
  -> ""
read 0 bytes
Conn close
unknownskl commented 3 years ago

It looks like you are not posting the body as a json string. Can you convert the body to json first and try to send that to the endpoint?

pacMakaveli commented 3 years ago

Sure:

        body = {
          'RelyingParty' => 'http://auth.xboxlive.com',
          'TokenType' => 'JWT',
          'Properties' => {
            'AuthMethod' => 'RPS',
            'SiteName' => 'user.auth.xboxlive.com',
            'RpsTicket' => "d=#{ access_token }"
          }
        }.to_json

Updated code and update response:

https://login.live.com/oauth20_authorize.srf?client_id=''&response_type=code&approval_prompt=auto&scope=Xboxlive.signin+Xboxlive.offline_access&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fmy_auth%2Fxbox
Please input your code:
  M.R3_BAY.14a0e063-d5c2-0e0b-ba5d-2312b9fdc4ef
opening connection to user.auth.xboxlive.com:443...
  opened
starting SSL for user.auth.xboxlive.com:443...
  SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384
<- "POST /user/authenticate HTTP/1.1\r\nX-Xbl-Contract-Version: 1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: user.auth.xboxlive.com\r\nContent-Length: 1236\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\n"
<- "{\"RelyingParty\":\"http://auth.xboxlive.com\",\"TokenType\":\"JWT\",\"Properties\":{\"AuthMethod\":\"RPS\",\"SiteName\":\"user.auth.xboxlive.com\",\"RpsTicket\":\"d=EwAwA+pvBAAUKods63Ys1fGlwiccIFJ+qE1hANsAAUEjM0aHApvcV+Wm4Z2Oax8ux3G0HcYZw9tBjtKYpMsikcv4Rs9GF44gjRrrQQPQ6y+eiF+9iWJbNRbzMo5vGbjY2dO/QL5TXexrEGu/94mUnT2F4k7PNcUl7clEnc0QfE4ANDOjxefmdRFA6ECe9aGalv4mLeDeJEc07xzjm/oV8ROIcYXZz4AJRFzu57kp2FcmfuE2TGxbds32Lq0SsbeTeNP5GNLg+9rsHyf92+BVva3Ylkmx3t5IVfoQ9O1Ila4hwHdGT4NvSs7/gPQcUw94zj2dGsj4ONeZXpJUSJe7x15It5ox9/ls8/BFcO3AwM/BSYWe4ZwEcq8GrSfpvKYDZgAACLZPXT1cFMMZAAK901k7ZxOUMteDJt7jw7TREWnBFEJuPjDADhnD76ifFkBPldEnYwQFhqBv36gx8R4Ure8l71nELhud5/HVn6K1bU6+cwuaCrZTfQfz0kNhbvXc9f5yBxv3tEFnx00y76EwME8HO/ZDZWAUME9t4Y+qzqIodIjO5z7eIdXeCREgslbTrhvWMdO9PydR7X2zOewE8a9vdAiq/fTeJa/ZiSzETzui+ZfsS8yCUiKWE00oAEmBWj6ZsCdYGkjmKTbjTQLTXyamO4Qi3ANqitycPJ9fDk2s0UOSLDlZND07HPrrfVx3MOaOOaAyBrksghEXqIn5+3R+ewcv740a1s728aHpybFgg8h0KIFLgwtjnVeC935BvqmvlpmGYvIxxo73eWOz2KaUuZkf68URns74CT+vl2mDp3WMQxuETh05q+qzQR+bKNEuw6duYD089j/+Zb2maAJuWL3MKCIbvE2fKvKG0jFqjjEoJ9IBCOKwS+hSVsGhH7ZCdbBF92Ae4X3ecbfh1DgRvldyYAt1zGt/FOennS8AdOvEUNUNzkIvecOjtGw0JlIRfcSqlz4ZN0CuLQ5cdEVUA+dpt/uDvXJ2B0lFh8iQWTAIozAygo2G832wVEMN4xD5MOZUgbJsg4FsA3JXGdaRtUdrCzSvWzeXewvoBjx271iVZf9G1pRCL+A8eS0C\"}}"
-> "HTTP/1.1 400 Bad Request\r\n"
-> "Cache-Control: no-cache, no-store\r\n"
-> "Content-Length: 0\r\n"
-> "MS-CV: wqynAfCIDEqduFejnfmwYA.0\r\n"
-> "X-Content-Type-Options: nosniff\r\n"
-> "X-XblCorrelationId: 00000000-0000-0000-0000-000000000000\r\n"
-> "Date: Thu, 14 Jan 2021 13:13:05 GMT\r\n"
-> "Connection: close\r\n"
-> "\r\n"
reading 0 bytes...
  -> ""
read 0 bytes
Conn close
pacMakaveli commented 3 years ago

If it helps, also:

Screenshot 2021-01-14 at 13 16 05

pacMakaveli commented 3 years ago

I just ran your library and it seems to work with my client id and redirect_uri , so the problem must be on my end in my post request.

I'll start comparing the requests and see what's different. I'll close this as it's on my end but if I sort it, I will come back with my fix and hopefully save some time for others that are dealing with the same issue.

Thanks for getting back @unknownskl

pacMakaveli commented 3 years ago

I could've sworn that this was my first 'fix' I tried when I was getting this issue. Remember to send JSON, kids!

        headers = {
          'Content-Type' => 'application/json',
          'x-xbl-contract-version' => '1'
        }
nightsurge commented 3 years ago

@pacMakaveli did you ever finish the port to Ruby? I would be really interested as I also use Ruby/Rails for my main app and want to use this authentication setup!

pacMakaveli commented 3 years ago

Sure did! I'm not going to post the gem I'm currently using because of personal reasons, but this is the full relevant piece of code which should get you up and running.

require 'httparty'

module XBOX
  module Oauth
    class Client
      include HTTParty

      DEFAULT_SCOPES ||= ['XboxLive.signin', 'XboxLive.offline_access'].join(' ').freeze

      USER_AGENT ||= [
        'Mozilla/5.0 (games.directory/2.0; XboxLiveAuth/3.0)',
        'AppleWebKit/537.36 (KHTML, like Gecko)',
        'Chrome/71.0.3578.98 Safari/537.36'
      ].join(' ').freeze

      CLIENTS ||= {
        MY_XBOX_LIVE: '0000000048093EE3',
        XBOX_APP: '000000004C12AE6F'
      }.freeze

      def self.authorization_url
        url = self.new.oauth_authorize_url

        if XBOX::Oauth.configuration.debug
          puts "Follow this link '#{ url }' and input the code returned below."
          puts 'Code: '
          code = gets.rstrip

          # Start the Authentication flow
          #
          self.new.authenticate(code)
        end

        url
      end

      def initialize
        raise 'Please read the README.md on how to configure the XBOX::Oauth module.' unless XBOX::Oauth.valid?
      end

      def authenticate(code, refresh: false)

        # Start the Authentication flow
        #
        access_token, refresh_token = request_oauth_token(code, refresh: refresh)
        user_token = get_user_token(access_token)
        session = get_xsts_token(user_token)

        # Return a new Session containing all the required information to start communicating with XBOX Live API
        #
        XBOX::Oauth::Session.new(session, access_token, refresh_token)
      end

      def refresh(refresh_token)
        authenticate(refresh_token, refresh: true)
      end

      # TODO: Add a way for the User to request the app be removed from their allowed oauth services
      #
      def delete(refresh_toke)
        # self.class.delete()
      end

      ### Authorize the account for the app in order to request an authorization code.
      #
      # Redirect the client to Microsoft' website where he will be prompted for login details.
      # If the Azure app has been configured correctly, on successful login, Microsoft will redirect back to the app using
      # the @redirect_uri configuration value along with a code which has to be passed to @oauth20_token
      #
      # @return Encoded URL to be used in redirecting the client
      # @example https://login.live.com/oauth20_authorize.srf?client_id=client_id&response_type=code&approval_prompt=auto&scope=Xboxlive.signin+Xboxlive.offline_access&redirect_uri=redirect_uri
      #
      def oauth_authorize_url
        params = {
          client_id: XBOX::Oauth.configuration.client_id,
          response_type: 'code',
          approval_prompt: 'auto',
          scope: DEFAULT_SCOPES,
          redirect_uri: XBOX::Oauth.configuration.redirect_uri
        }

        "https://login.live.com/oauth20_authorize.srf?#{ URI.encode_www_form(params) }"
      end

      ## Authenticate the account via the authorization code returned by @oauth_authorize_url and receive an access and
      # refresh token
      #
      # @return [Hash] containing the access_token, refresh_token, expiration_date, etc..
      # @example
      #
      def request_oauth_token(code, refresh: false)
        raise '"code" is empty; you need it silly! Did you forget to launch @authorization_url?' unless code

        body = {
          grant_type: (refresh ? 'refresh_token' : 'authorization_code'),
          client_id: XBOX::Oauth.configuration.client_id,
          scope: DEFAULT_SCOPES,
          redirect_uri: XBOX::Oauth.configuration.redirect_uri,
        }

        body[refresh ? 'refresh_token' : 'code'] = code
        body[:client_secret] = client_secret unless (client_secret = XBOX::Oauth.configuration.client_secret)

        request = self.class.post('https://login.live.com/oauth20_token.srf',
          body: body
        ).parsed_response

        raise request['error_description'] if request.has_key?('error')

        return request['access_token'], request['refresh_token']
      end

      # Authenticate with XBOX Live using the user's access_token returned by @request_oauth_token and receive a
      # user_token and uhs id which are to be used with @get_xsts_token
      #
      # @return
      # @example
      #
      def get_user_token(access_token)
        raise '"access_token" is empty; you need it silly!' unless access_token

        request = self.class.post('https://user.auth.xboxlive.com/user/authenticate',
          body: {
            'RelyingParty' => 'http://auth.xboxlive.com',
            'TokenType' => 'JWT',
            'Properties' => {
              'AuthMethod' => 'RPS',
              'SiteName' => 'user.auth.xboxlive.com',
              'RpsTicket' => "d=#{ access_token }"
            }
          }.to_json,

          headers: {
            'Content-Type' => 'application/json',
            'x-xbl-contract-version' => '1'
          }
        )

        request.parsed_response['Token']
      end

      # Authorize via user token and receive final X token
      #
      # @return [Hash]
      #
      def get_xsts_token(user_tokens = [])
        raise '"user_tokens" is empty; you need it silly!' if user_tokens.empty?

        request = self.class.post('https://xsts.auth.xboxlive.com/xsts/authorize',
          body: {
            'RelyingParty' => 'http://xboxlive.com',
            'TokenType' => 'JWT',
            'Properties' => {
              'UserTokens' => [user_tokens],
              # 'DeviceToken' => '',
              # 'TitleToken' => '',
              # 'OptionalDisplayClaims' => [''],
              'SandboxId' => 'RETAIL'
            }
          }.to_json,

          headers: {
            'Content-Type' => 'application/json',
            'x-xbl-contract-version' => '1'
          }
        )

        raise 'An error occured in get_xsts_token' if request.code != 200

        request.parsed_response
      end
    end
  end
end

And this is how I use it in my app


# Placeholder for the XBOX Authentication service
# REVIEW: Create an omniauth-xbox gem wrapper for xbox-oauth and let omniauth deal with this
#
class MyAuthController < ApplicationController
  def xbox
    if (code = params[:code])
      request = XBOX::Oauth::Client.new.authenticate(code)
      user = User.find_for_oauth(request, 'xbox', current_user)

      if user
        sign_in(user, event: :authentication)

        # Quick hack for allowing users to login with XBOX Live; No point in trying to figure
        # out a nicer way since all this will be handled by omniauth.
        #
        redirect_to user_url(current_user), fallback_location: root_url
      end
    else
      redirect_to XBOX::Oauth::Client.authorization_url
    end
  end
end