Ctrl K
ComunidadGuías…

test/integration/oauth_audit_test.rb

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
require "test_helper"

# Each OAuth lifecycle change reports its audit event (AU-1, Oauth::Audit),
# with the standard fields and never a credential.
class OauthAuditTest < ActionDispatch::IntegrationTest
  include OauthAuditTestHelper

  VERIFIER = "dBjftJeZ4CVP-mJ92K9OsQdq2VqrkaKUX38OaoPvBqg"
  CHALLENGE = Base64.urlsafe_encode64(Digest::SHA256.digest(VERIFIER), padding: false)
  REDIRECT_URI = "http://127.0.0.1:8888/callback"

  setup do
    @client = oauth_clients(:mcp_client)
    @identity = identities(:david)
  end

  test "showing, approving and denying consent" do
    sign_in_as :david

    events = capture_oauth_events do
      untenanted { get new_oauth_authorization_path, params: consent_params }
    end
    shown = oauth_event(events, "oauth.consent.shown")
    assert_equal @client.client_id, shown[:client_id]
    assert_equal "dcr", shown[:client_class]
    assert_equal "public", shown[:client_type]
    assert_equal @identity.id, shown[:identity_id]
    assert_equal "user", shown[:actor]
    assert_equal response.request.uuid, shown[:request_id]

    events = capture_oauth_events do
      untenanted { post oauth_authorization_path, params: consent_params }
    end
    assert_equal %w[ oauth.consent.approved oauth.code.issued ], oauth_event_names(events)
    code = Rack::Utils.parse_query(URI.parse(response.location).query)["code"]
    assert_no_secrets_in events, code

    events = capture_oauth_events do
      untenanted { post oauth_authorization_path, params: consent_params(error: "access_denied") }
    end
    assert_equal "user", oauth_event(events, "oauth.consent.denied")[:actor]
  end

  test "an error before the consent screen shows no consent" do
    sign_in_as :david

    events = capture_oauth_events do
      untenanted { get new_oauth_authorization_path, params: consent_params(code_challenge: nil) }
    end

    assert_empty events
  end

  test "exchanging a code redeems it, creates a grant and issues tokens" do
    events = capture_oauth_events { exchange authorization_code }

    assert_response :success
    grant = Identity::AccessToken.find_by_raw_token(response.parsed_body["access_token"])
    assert_equal %w[ oauth.code.redeemed oauth.grant.created oauth.token.issued ], oauth_event_names(events)

    issued = oauth_event(events, "oauth.token.issued")
    assert_equal grant.id, issued[:grant_id]
    assert_equal grant.id, issued[:family_id]
    assert_equal @identity.id, issued[:identity_id]
    assert_equal "authorization_code", issued[:grant_type]
    assert_equal "client", issued[:actor]
    assert_nil issued[:tenant_id]
    assert_no_secrets_in events, response.parsed_body["access_token"], response.parsed_body["refresh_token"]
  end

  test "a replayed code revokes the grant it issued" do
    code = authorization_code
    exchange code
    grant_id = Identity::AccessToken.find_by_raw_token(response.parsed_body["access_token"]).id

    events = capture_oauth_events { exchange code }

    assert_response :bad_request
    assert_equal %w[ oauth.code.replayed oauth.grant.revoked ], oauth_event_names(events)
    assert_equal grant_id, oauth_event(events, "oauth.code.replayed")[:grant_id]

    revoked = oauth_event(events, "oauth.grant.revoked")
    assert_equal grant_id, revoked[:grant_id]
    assert_equal "code_replayed", revoked[:reason]
    assert_equal "system", revoked[:actor]

    events = capture_oauth_events { exchange code }
    third = oauth_event(events, "oauth.code.replayed")
    assert_nil third[:grant_id]
    assert_equal @identity.id, third[:identity_id]
  end

  test "refreshing, a retry within the grace window, and reuse past it" do
    freeze_time
    grant = @identity.access_tokens.create!(oauth_client: @client, permission: :read)
    presented = grant.raw_refresh_token

    events = capture_oauth_events { refresh presented }
    refreshed = oauth_event(events, "oauth.token.refreshed")
    assert_equal grant.id, refreshed[:grant_id]
    assert_equal "refresh_token", refreshed[:grant_type]
    successor = response.parsed_body
    assert_no_secrets_in events, presented, successor["access_token"], successor["refresh_token"]

    events = capture_oauth_events { refresh presented }
    assert_response :success
    assert_equal "answered", oauth_event(events, "oauth.token.grace_replay")[:reason]
    assert_no_secrets_in events, presented, successor["access_token"], successor["refresh_token"]

    travel Oauth::RetiredRefreshToken::GRACE
    events = capture_oauth_events { refresh presented }

    assert_equal %w[ oauth.token.reuse_detected oauth.grant.revoked ], oauth_event_names(events)
    revoked = oauth_event(events, "oauth.grant.revoked")
    assert_equal "reuse_detected", revoked[:reason]
    assert_equal "system", revoked[:actor]
  end

  test "a refresh that narrows scope narrows the grant" do
    grant = @identity.access_tokens.create!(oauth_client: @client, permission: :write)

    events = capture_oauth_events { refresh grant.raw_refresh_token, scope: "read" }

    assert_equal %w[ oauth.token.refreshed oauth.grant.narrowed ], oauth_event_names(events)
    assert_equal "read", oauth_event(events, "oauth.grant.narrowed")[:permission]

    events = capture_oauth_events { refresh response.parsed_body["refresh_token"], scope: "read" }
    assert_equal %w[ oauth.token.refreshed ], oauth_event_names(events)
  end

  test "a retry within the grace window that can't be answered says why" do
    freeze_time
    grant = @identity.access_tokens.create!(oauth_client: @client, permission: :write)
    presented = grant.raw_refresh_token
    refresh presented

    events = capture_oauth_events { refresh presented, scope: "read" }
    assert_equal "scope_mismatch", oauth_event(events, "oauth.token.grace_replay")[:reason]

    Oauth::RetiredRefreshToken.update_all successor_payload: nil
    events = capture_oauth_events { refresh presented }
    assert_equal "successor_gone", oauth_event(events, "oauth.token.grace_replay")[:reason]
  end

  test "a retry within the grace window asking for more than the grant is a replay, and refused" do
    freeze_time
    grant = @identity.access_tokens.create!(oauth_client: @client, permission: :read)
    presented = grant.raw_refresh_token
    refresh presented

    [ "read write", "", "admin" ].each do |scope|
      events = capture_oauth_events { refresh presented, scope: scope }

      assert_response :bad_request
      assert_equal "invalid_scope", response.parsed_body["error"]
      assert_equal %w[ oauth.token.grace_replay ], oauth_event_names(events), "scope #{scope.inspect}"
      assert_equal "scope_mismatch", oauth_event(events, "oauth.token.grace_replay")[:reason]
    end
  end

  test "a retry within the grace window after its successor has rotated is superseded" do
    freeze_time
    grant = @identity.access_tokens.create!(oauth_client: @client, permission: :read)
    presented = grant.raw_refresh_token
    refresh presented
    refresh response.parsed_body["refresh_token"]

    events = capture_oauth_events { refresh presented }

    assert_equal "superseded", oauth_event(events, "oauth.token.grace_replay")[:reason]
    assert Identity::AccessToken.exists?(grant.id)
  end

  test "revoking at the revocation endpoint, by either token" do
    { "access" => :raw_token, "refresh" => :raw_refresh_token }.each do |presented, token|
      grant = @identity.access_tokens.create!(oauth_client: @client, permission: :read)

      events = capture_oauth_events { revoke grant.public_send(token) }

      revoked = oauth_event(events, "oauth.grant.revoked")
      assert_equal grant.id, revoked[:grant_id]
      assert_equal "revocation_endpoint", revoked[:reason]
      assert_equal "client", revoked[:actor]
      assert_equal presented, revoked[:presented]
      assert_no_secrets_in events, grant.raw_token, grant.raw_refresh_token
    end
  end

  test "revoking a personal access token at the revocation endpoint" do
    pat = @identity.access_tokens.create!(permission: :read)

    events = capture_oauth_events { revoke pat.raw_token }

    revoked = oauth_event(events, "oauth.pat.revoked")
    assert_equal "pat", revoked[:client_class]
    assert_nil revoked[:client_id]
    assert_equal "access", revoked[:presented]
    assert_no_secrets_in events, pat.raw_token
  end

  test "an unknown token revokes nothing and reports nothing" do
    assert_empty capture_oauth_events { revoke "fizzy_at_unknown" }
  end

  test "disconnecting an app revokes each of its grants" do
    sign_in_as :david
    grants = 2.times.map { @identity.access_tokens.create!(oauth_client: @client, permission: :read) }

    events = capture_oauth_events { delete my_connected_app_path(@client) }

    assert_equal %w[ oauth.grant.revoked oauth.grant.revoked ], oauth_event_names(events)
    assert_equal grants.map(&:id).sort, events.map { |event| event[:payload][:grant_id] }.sort
    events.each do |event|
      assert_equal "disconnected", event[:payload][:reason]
      assert_equal "user", event[:payload][:actor]
    end
  end

  test "creating and revoking a personal access token" do
    sign_in_as :david

    events = capture_oauth_events do
      post my_access_tokens_path, params: { access_token: { description: "CLI", permission: "write" } }, as: :json
    end
    created = oauth_event(events, "oauth.pat.created")
    assert_equal "pat", created[:client_class]
    assert_equal "user", created[:actor]
    assert_equal "write", created[:permission]
    assert_no_secrets_in events, response.parsed_body["token"]

    events = capture_oauth_events { delete my_access_token_path(response.parsed_body["id"]), as: :json }
    revoked = oauth_event(events, "oauth.pat.revoked")
    assert_equal created[:grant_id], revoked[:grant_id]
    assert_equal "revoked", revoked[:reason]
    assert_equal "user", revoked[:actor]
  end

  test "registering a client attributes it to no one signed in" do
    sign_in_as :david

    events = capture_oauth_events do
      untenanted do
        post oauth_clients_path, as: :json, params: \
          { client_name: "Connector", redirect_uris: %w[ https://connector.example.com/callback ], token_endpoint_auth_method: "client_secret_post" }
      end
    end

    assert_response :created
    registered = oauth_event(events, "oauth.client.registered")
    assert_equal response.parsed_body["client_id"], registered[:client_id]
    assert_equal "dcr", registered[:client_class]
    assert_equal "confidential", registered[:client_type]
    assert_equal "client", registered[:actor]
    assert_nil registered[:identity_id]
    assert_no_secrets_in events, response.parsed_body["client_secret"]
  end

  test "a rejected registration registers nothing" do
    events = capture_oauth_events do
      untenanted { post oauth_clients_path, as: :json, params: { redirect_uris: %w[ http://example.com/callback ] } }
    end

    assert_response :bad_request
    assert_empty events
  end

  private
    def consent_params(**overrides)
      { client_id: @client.client_id, redirect_uri: REDIRECT_URI, response_type: "code", code_challenge: CHALLENGE,
        code_challenge_method: "S256", scope: "read", state: "xyz123" }.merge(overrides).compact
    end

    def authorization_code
      Oauth::AuthorizationCode.generate client_id: @client.client_id, identity_id: @identity.id,
        code_challenge: CHALLENGE, redirect_uri: REDIRECT_URI, scope: "read"
    end

    def exchange(code)
      untenanted do
        post oauth_token_path, params: \
          { grant_type: "authorization_code", code: code, code_verifier: VERIFIER, redirect_uri: REDIRECT_URI, client_id: @client.client_id }
      end
    end

    def refresh(refresh_token, **params)
      untenanted do
        post oauth_token_path, params: { grant_type: "refresh_token", refresh_token: refresh_token, client_id: @client.client_id, **params }
      end
    end

    def revoke(token)
      untenanted { post oauth_revocation_path, params: { token: token, client_id: @client.client_id } }
    end
end