# frozen_string_literal: true

require "spec_helper"

feature "Authorization Code Flow" do
  background do
    default_scopes_exist :default
    config_is_set(:authenticate_resource_owner) { User.first || redirect_to("/sign_in") }
    client_exists
    create_resource_owner
    sign_in
  end

  scenario "resource owner authorizes the client" do
    visit authorization_endpoint_url(client: @client)
    click_on "Authorize"

    access_grant_should_exist_for(@client, @resource_owner)

    i_should_be_on_client_callback(@client)

    url_should_have_param("code", Doorkeeper::AccessGrant.first.token)
    url_should_not_have_param("state")
    url_should_not_have_param("error")
  end

  context "when configured to check application supported grant flow" do
    before do
      config_is_set(:allow_grant_flow_for_client, ->(_grant_flow, client) { client.name == "admin" })
    end

    scenario "forbids the request when doesn't satisfy condition" do
      @client.update(name: "sample app")

      visit authorization_endpoint_url(client: @client)

      i_should_see_translated_error_message("unauthorized_client")
    end

    scenario "allows the request when satisfies condition" do
      @client.update(name: "admin")

      visit authorization_endpoint_url(client: @client)
      i_should_not_see_translated_error_message("unauthorized_client")
      click_on "Authorize"

      authorization_code = Doorkeeper::AccessGrant.first.token
      create_access_token authorization_code, @client

      access_token_should_exist_for(@client, @resource_owner)

      should_not_have_json "error"

      should_have_json "access_token", Doorkeeper::AccessToken.first.token
      should_have_json "token_type", "Bearer"
      should_have_json_within "expires_in", Doorkeeper::AccessToken.first.expires_in, 1
    end
  end

  context "with grant hashing enabled" do
    background do
      config_is_set(:token_secret_strategy, ::Doorkeeper::SecretStoring::Sha256Hash)
    end

    def authorize(redirect_url)
      @client.redirect_uri = redirect_url
      @client.save!
      visit authorization_endpoint_url(client: @client)
      click_on "Authorize"

      access_grant_should_exist_for(@client, @resource_owner)

      code = current_params["code"]
      expect(code).not_to be_nil

      hashed_code = Doorkeeper::AccessGrant.secret_strategy.transform_secret code
      expect(hashed_code).to eq Doorkeeper::AccessGrant.first.token

      [code, hashed_code]
    end

    scenario "using redirect_url urn:ietf:wg:oauth:2.0:oob" do
      code, hashed_code = authorize("urn:ietf:wg:oauth:2.0:oob")
      expect(code).not_to eq(hashed_code)
      i_should_see "Authorization code:"
      i_should_see code
      i_should_not_see hashed_code
    end

    scenario "using redirect_url urn:ietf:wg:oauth:2.0:oob:auto" do
      code, hashed_code = authorize("urn:ietf:wg:oauth:2.0:oob:auto")
      expect(code).not_to eq(hashed_code)
      i_should_see "Authorization code:"
      i_should_see code
      i_should_not_see hashed_code
    end
  end

  scenario "resource owner authorizes using oob url" do
    @client.redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
    @client.save!
    visit authorization_endpoint_url(client: @client)
    click_on "Authorize"

    access_grant_should_exist_for(@client, @resource_owner)

    url_should_have_param("code", Doorkeeper::AccessGrant.first.token)
    i_should_see "Authorization code:"
    i_should_see Doorkeeper::AccessGrant.first.token
  end

  scenario "resource owner authorizes the client with state parameter set" do
    visit authorization_endpoint_url(client: @client, state: "return-me")
    click_on "Authorize"
    url_should_have_param("code", Doorkeeper::AccessGrant.first.token)
    url_should_have_param("state", "return-me")
    url_should_not_have_param("code_challenge_method")
  end

  scenario "resource owner requests an access token without authorization code" do
    create_access_token "", @client

    access_token_should_not_exist

    expect(Doorkeeper::AccessToken.count).to be_zero

    should_have_json "error", "invalid_request"
    should_have_json "error_description", translated_invalid_request_error_message(:missing_param, :code)
  end

  scenario "resource owner requests an access token with authorization code" do
    visit authorization_endpoint_url(client: @client)
    click_on "Authorize"

    authorization_code = Doorkeeper::AccessGrant.first.token
    create_access_token authorization_code, @client

    access_token_should_exist_for(@client, @resource_owner)

    should_not_have_json "error"

    should_have_json "access_token", Doorkeeper::AccessToken.first.token
    should_have_json "token_type", "Bearer"
    should_have_json_within "expires_in", Doorkeeper::AccessToken.first.expires_in, 1
  end

  scenario "resource owner requests an access token with authorization code but without secret" do
    visit authorization_endpoint_url(client: @client)
    click_on "Authorize"

    authorization_code = Doorkeeper::AccessGrant.first.token
    page.driver.post token_endpoint_url(
      code: authorization_code, client_id: @client.uid,
      redirect_uri: @client.redirect_uri,
    )

    expect(Doorkeeper::AccessToken.count).to be_zero

    should_have_json "error", "invalid_client"
    should_have_json "error_description", translated_error_message(:invalid_client)
  end

  scenario "resource owner requests an access token with authorization code but without client id" do
    visit authorization_endpoint_url(client: @client)
    click_on "Authorize"

    authorization_code = Doorkeeper::AccessGrant.first.token
    page.driver.post token_endpoint_url(
      code: authorization_code, client_secret: @client.secret,
      redirect_uri: @client.redirect_uri,
    )

    expect(Doorkeeper::AccessToken.count).to be_zero

    should_have_json "error", "invalid_client"
    should_have_json "error_description", translated_error_message(:invalid_client)
  end

  scenario "silently authorizes if matching token exists" do
    default_scopes_exist :public, :write

    access_token_exists application: @client,
                        expires_in: -100, # even expired token
                        resource_owner_id: @resource_owner.id,
                        scopes: "public write"

    visit authorization_endpoint_url(client: @client, scope: "public write")

    response_status_should_be 200
    i_should_not_see "Authorize"
  end

  context "with PKCE" do
    context "plain" do
      let(:code_challenge) { "a45a9fea-0676-477e-95b1-a40f72ac3cfb" }
      let(:code_verifier) { "a45a9fea-0676-477e-95b1-a40f72ac3cfb" }

      scenario "resource owner authorizes the client with code_challenge parameter set" do
        visit authorization_endpoint_url(
          client: @client,
          code_challenge: code_challenge,
          code_challenge_method: "plain",
        )
        click_on "Authorize"

        url_should_have_param("code", Doorkeeper::AccessGrant.first.token)
        url_should_not_have_param("code_challenge_method")
        url_should_not_have_param("code_challenge")
      end

      scenario "mobile app requests an access token with authorization code but not pkce token" do
        visit authorization_endpoint_url(client: @client)
        click_on "Authorize"

        authorization_code = current_params["code"]
        create_access_token authorization_code, @client, code_verifier

        should_have_json "error", "invalid_grant"
        should_have_json "error_description", translated_error_message(:invalid_grant)
      end

      scenario "mobile app requests an access token with authorization code and plain code challenge method" do
        visit authorization_endpoint_url(
          client: @client,
          code_challenge: code_challenge,
          code_challenge_method: "plain",
        )
        click_on "Authorize"

        authorization_code = current_params["code"]
        create_access_token authorization_code, @client, code_verifier

        access_token_should_exist_for(@client, @resource_owner)

        should_not_have_json "error"

        should_have_json "access_token", Doorkeeper::AccessToken.first.token
        should_have_json "token_type", "Bearer"
        should_have_json_within "expires_in", Doorkeeper::AccessToken.first.expires_in, 1
      end

      scenario "mobile app requests an access token with authorization code but without code_verifier" do
        visit authorization_endpoint_url(
          client: @client,
          code_challenge: code_challenge,
          code_challenge_method: "plain",
        )
        click_on "Authorize"

        authorization_code = current_params["code"]
        create_access_token authorization_code, @client, nil

        should_not_have_json "access_token"
        should_have_json "error", "invalid_request"
        should_have_json "error_description", translated_invalid_request_error_message(:missing_param, :code_verifier)
      end

      scenario "mobile app requests an access token with authorization code with wrong code_verifier" do
        visit authorization_endpoint_url(
          client: @client,
          code_challenge: code_challenge,
          code_challenge_method: "plain",
        )
        click_on "Authorize"

        authorization_code = current_params["code"]
        create_access_token authorization_code, @client, "wrong_code_verifier"

        should_not_have_json "access_token"
        should_have_json "error", "invalid_grant"
        should_have_json "error_description", translated_error_message(:invalid_grant)
      end
    end

    context "s256" do
      let(:code_challenge) { "Oz733NtQ0rJP8b04fgZMJMwprn6Iw8sMCT_9bR1q4tA" }
      let(:code_verifier) { "a45a9fea-0676-477e-95b1-a40f72ac3cfb" }

      scenario "resource owner authorizes the client with code_challenge parameter set" do
        visit authorization_endpoint_url(
          client: @client,
          code_challenge: code_challenge,
          code_challenge_method: "S256",
        )
        click_on "Authorize"

        url_should_have_param("code", Doorkeeper::AccessGrant.first.token)
        url_should_not_have_param("code_challenge_method")
        url_should_not_have_param("code_challenge")
      end

      scenario "mobile app requests an access token with authorization code and S256 code challenge method" do
        visit authorization_endpoint_url(
          client: @client,
          code_challenge: code_challenge,
          code_challenge_method: "S256",
        )
        click_on "Authorize"

        authorization_code = current_params["code"]
        create_access_token authorization_code, @client, code_verifier

        access_token_should_exist_for(@client, @resource_owner)

        should_not_have_json "error"

        should_have_json "access_token", Doorkeeper::AccessToken.first.token
        should_have_json "token_type", "Bearer"
        should_have_json_within "expires_in", Doorkeeper::AccessToken.first.expires_in, 1
      end

      scenario "mobile app requests an access token with authorization code and without secret" do
        visit authorization_endpoint_url(
          client: @client,
          code_challenge: code_challenge,
          code_challenge_method: "S256",
        )
        click_on "Authorize"

        authorization_code = current_params["code"]
        page.driver.post token_endpoint_url(
          code: authorization_code,
          client_id: @client.uid,
          redirect_uri: @client.redirect_uri,
          code_verifier: code_verifier,
        )
        should_not_have_json "access_token"
        should_have_json "error", "invalid_client"
        should_have_json "error_description", translated_error_message(:invalid_client)
      end

      scenario "mobile app requests an access token with authorization code and without secret but is marked as not confidential" do
        @client.update_attribute :confidential, false
        visit authorization_endpoint_url(client: @client, code_challenge: code_challenge, code_challenge_method: "S256")
        click_on "Authorize"

        authorization_code = current_params["code"]
        page.driver.post token_endpoint_url(
          code: authorization_code,
          client_id: @client.uid,
          redirect_uri: @client.redirect_uri,
          code_verifier: code_verifier,
        )
        should_not_have_json "error"

        should_have_json "access_token", Doorkeeper::AccessToken.first.token
        should_have_json "token_type", "Bearer"
        should_have_json_within "expires_in", Doorkeeper::AccessToken.first.expires_in, 1
      end

      scenario "mobile app requests an access token with authorization code but no code verifier" do
        visit authorization_endpoint_url(
          client: @client,
          code_challenge: code_challenge,
          code_challenge_method: "S256",
        )
        click_on "Authorize"

        authorization_code = current_params["code"]
        create_access_token authorization_code, @client

        should_not_have_json "access_token"
        should_have_json "error", "invalid_request"
        should_have_json "error_description", translated_invalid_request_error_message(:missing_param, :code_verifier)
      end

      scenario "mobile app requests an access token with authorization code with wrong verifier" do
        visit authorization_endpoint_url(
          client: @client,
          code_challenge: code_challenge,
          code_challenge_method: "S256",
        )
        click_on "Authorize"

        authorization_code = current_params["code"]
        create_access_token authorization_code, @client, "incorrect-code-verifier"

        should_not_have_json "access_token"
        should_have_json "error", "invalid_grant"
        should_have_json "error_description", translated_error_message(:invalid_grant)
      end

      scenario "code_challenge_mehthod in token request is totally ignored" do
        visit authorization_endpoint_url(
          client: @client,
          code_challenge: code_challenge,
          code_challenge_method: "S256",
        )
        click_on "Authorize"

        authorization_code = current_params["code"]
        page.driver.post token_endpoint_url(
          code: authorization_code,
          client: @client,
          code_verifier: code_challenge,
          code_challenge_method: "plain",
        )

        should_not_have_json "access_token"
        should_have_json "error", "invalid_grant"
        should_have_json "error_description", translated_error_message(:invalid_grant)
      end

      scenario "expects to set code_challenge_method explicitely without fallback" do
        visit authorization_endpoint_url(client: @client, code_challenge: code_challenge)
        expect(page).to have_content("The code challenge method must be plain or S256.")
      end
    end
  end

  context "when application scopes are present and no scope is passed" do
    background do
      @client.update(scopes: "public write read default")
    end

    scenario "scope is invalid because default scope is different from application scope" do
      default_scopes_exist :admin
      visit authorization_endpoint_url(client: @client)
      response_status_should_be 200
      i_should_not_see "Authorize"
      i_should_see_translated_error_message :invalid_scope
    end

    scenario "access grant have scopes which are common in application scopees and default scopes" do
      default_scopes_exist :public, :write
      visit authorization_endpoint_url(client: @client)
      click_on "Authorize"
      access_grant_should_exist_for(@client, @resource_owner)
      access_grant_should_have_scopes :public, :write
    end
  end

  context "with scopes" do
    background do
      default_scopes_exist :public
      optional_scopes_exist :write
    end

    scenario "resource owner authorizes the client with default scopes" do
      visit authorization_endpoint_url(client: @client)
      click_on "Authorize"
      access_grant_should_exist_for(@client, @resource_owner)
      access_grant_should_have_scopes :public
    end

    scenario "resource owner authorizes the client with required scopes" do
      visit authorization_endpoint_url(client: @client, scope: "public write")
      click_on "Authorize"
      access_grant_should_have_scopes :public, :write
    end

    scenario "resource owner authorizes the client with required scopes (without defaults)" do
      visit authorization_endpoint_url(client: @client, scope: "write")
      click_on "Authorize"
      access_grant_should_have_scopes :write
    end

    scenario "new access token matches required scopes" do
      visit authorization_endpoint_url(client: @client, scope: "public write")
      click_on "Authorize"

      authorization_code = Doorkeeper::AccessGrant.first.token
      create_access_token authorization_code, @client

      access_token_should_exist_for(@client, @resource_owner)
      access_token_should_have_scopes :public, :write
    end

    scenario "returns new token if scopes have changed" do
      client_is_authorized(@client, @resource_owner, scopes: "public write")
      visit authorization_endpoint_url(client: @client, scope: "public")
      click_on "Authorize"

      authorization_code = Doorkeeper::AccessGrant.first.token
      create_access_token authorization_code, @client

      expect(Doorkeeper::AccessToken.count).to be(2)

      should_have_json "access_token", Doorkeeper::AccessToken.last.token
    end

    scenario "resource owner authorizes the client with extra scopes" do
      client_is_authorized(@client, @resource_owner, scopes: "public")
      visit authorization_endpoint_url(client: @client, scope: "public write")
      click_on "Authorize"

      authorization_code = Doorkeeper::AccessGrant.first.token
      create_access_token authorization_code, @client

      expect(Doorkeeper::AccessToken.count).to be(2)

      should_have_json "access_token", Doorkeeper::AccessToken.last.token
      access_token_should_have_scopes :public, :write
    end
  end
end

describe "Authorization Code Flow" do
  before do
    Doorkeeper.configure do
      orm DOORKEEPER_ORM
      use_refresh_token
    end

    client_exists
  end

  context "issuing a refresh token" do
    before do
      authorization_code_exists application: @client
    end

    it "second of simultaneous client requests get an error for revoked acccess token" do
      authorization_code = Doorkeeper::AccessGrant.first.token
      allow_any_instance_of(Doorkeeper::AccessGrant)
        .to receive(:revoked?).and_return(false, true)

      post token_endpoint_url(code: authorization_code, client: @client)

      should_not_have_json "access_token"
      should_have_json "error", "invalid_grant"
      should_have_json "error_description", translated_error_message(:invalid_grant)
    end
  end
end
