|
1 | 1 | # frozen_string_literal: true |
2 | 2 |
|
3 | 3 | require_relative 'helper' |
| 4 | +require 'base64' |
| 5 | +require 'digest' |
4 | 6 |
|
5 | 7 | class OAuthBaseTest < Minitest::Test |
6 | 8 | APP_ROOT = File.expand_path(File.join(__dir__, '../app')) |
@@ -418,3 +420,209 @@ def test_oauth_consent_endpoint_post_allow |
418 | 420 | assert_equal auth_params, code_info |
419 | 421 | end |
420 | 422 | end |
| 423 | + |
| 424 | +class OAuthPhase4AuthorizationTest < OAuthBaseTest |
| 425 | + def setup |
| 426 | + super |
| 427 | + @client_info = { |
| 428 | + "client_name" => "My AI Agent", |
| 429 | + "redirect_uris" => ["http://localhost:8400/callback"], |
| 430 | + "grant_types" => ["authorization_code", "refresh_token"] |
| 431 | + } |
| 432 | + @client_id = @store.store(@client_info) |
| 433 | + |
| 434 | + @session_info = { |
| 435 | + username: 'foobar' |
| 436 | + } |
| 437 | + @sid = @store.store(@session_info) |
| 438 | + |
| 439 | + @code_verifier = SecureRandom.hex(16) |
| 440 | + @code_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(@code_verifier), padding: false) |
| 441 | + |
| 442 | + @auth_params = { |
| 443 | + 'response_type' => 'code', |
| 444 | + 'client_id' => @client_id, |
| 445 | + 'redirect_uri' => 'http://localhost:4321/callback', |
| 446 | + 'code_challenge' => @code_challenge, |
| 447 | + 'code_challenge_method' => 'S256', |
| 448 | + 'state' => 'my_state', |
| 449 | + 'sid' => @sid |
| 450 | + } |
| 451 | + @auth_code = @store.store(@auth_params) |
| 452 | + end |
| 453 | + |
| 454 | + def test_oauth_token_exchange |
| 455 | + req = @test_harness.request( |
| 456 | + { |
| 457 | + ':method' => 'POST', |
| 458 | + ':path' => '/oauth/token', |
| 459 | + 'content-type' => 'application/x-www-form-urlencoded' |
| 460 | + }, |
| 461 | + URI.encode_www_form( |
| 462 | + grant_type: 'authorization_code', |
| 463 | + code: @auth_code, |
| 464 | + redirect_uri: 'http://localhost:4321/callback', |
| 465 | + client_id: @client_id, |
| 466 | + code_verifier: @code_verifier |
| 467 | + ) |
| 468 | + ) |
| 469 | + |
| 470 | + assert_equal HTTP::OK, req.response_status |
| 471 | + assert_equal 'application/json', req.response_content_type |
| 472 | + json = req.response_json |
| 473 | + |
| 474 | + at = json['access_token'] |
| 475 | + assert_kind_of String, at |
| 476 | + token_info = @store.fetch(at) |
| 477 | + refute_nil token_info |
| 478 | + assert_equal @session_info[:username], token_info[:username] |
| 479 | + assert_equal 'oauth', token_info[:type] |
| 480 | + |
| 481 | + assert_equal 'Bearer', json['token_type'] |
| 482 | + assert_kind_of Integer, json['expires_in'] |
| 483 | + end |
| 484 | + |
| 485 | + def test_oauth_token_exchange_missing_params |
| 486 | + req = @test_harness.request( |
| 487 | + { |
| 488 | + ':method' => 'POST', |
| 489 | + ':path' => '/oauth/token', |
| 490 | + 'content-type' => 'application/x-www-form-urlencoded' |
| 491 | + }, |
| 492 | + URI.encode_www_form({} |
| 493 | + # grant_type: 'authorization_code', |
| 494 | + # code: auth_code, |
| 495 | + # redirect_uri: 'http://localhost:4321/callback', |
| 496 | + # client_id: client_id, |
| 497 | + # code_verifier: code_verifier |
| 498 | + ) |
| 499 | + ) |
| 500 | + |
| 501 | + assert_equal HTTP::BAD_REQUEST, req.response_status |
| 502 | + assert_equal 'application/json', req.response_content_type |
| 503 | + json = req.response_json |
| 504 | + |
| 505 | + error = json['error'] |
| 506 | + assert_equal 'invalid_request', error |
| 507 | + end |
| 508 | + |
| 509 | + def test_oauth_token_exchange_invalid_grant_type |
| 510 | + req = @test_harness.request( |
| 511 | + { |
| 512 | + ':method' => 'POST', |
| 513 | + ':path' => '/oauth/token', |
| 514 | + 'content-type' => 'application/x-www-form-urlencoded' |
| 515 | + }, |
| 516 | + URI.encode_www_form( |
| 517 | + grant_type: 'foo', |
| 518 | + code: @auth_code, |
| 519 | + redirect_uri: 'http://localhost:4321/callback', |
| 520 | + client_id: @client_id, |
| 521 | + code_verifier: @code_verifier |
| 522 | + ) |
| 523 | + ) |
| 524 | + |
| 525 | + assert_equal HTTP::BAD_REQUEST, req.response_status |
| 526 | + assert_equal 'application/json', req.response_content_type |
| 527 | + json = req.response_json |
| 528 | + |
| 529 | + error = json['error'] |
| 530 | + assert_equal 'unsupported_grant_type', error |
| 531 | + end |
| 532 | + |
| 533 | + def test_oauth_token_exchange_invalid_code |
| 534 | + req = @test_harness.request( |
| 535 | + { |
| 536 | + ':method' => 'POST', |
| 537 | + ':path' => '/oauth/token', |
| 538 | + 'content-type' => 'application/x-www-form-urlencoded' |
| 539 | + }, |
| 540 | + URI.encode_www_form( |
| 541 | + grant_type: 'authorization_code', |
| 542 | + code: @auth_code + '!', |
| 543 | + redirect_uri: 'http://localhost:4321/callback', |
| 544 | + client_id: @client_id, |
| 545 | + code_verifier: @code_verifier |
| 546 | + ) |
| 547 | + ) |
| 548 | + |
| 549 | + assert_equal HTTP::BAD_REQUEST, req.response_status |
| 550 | + assert_equal 'application/json', req.response_content_type |
| 551 | + json = req.response_json |
| 552 | + |
| 553 | + error = json['error'] |
| 554 | + assert_equal 'invalid_request', error |
| 555 | + end |
| 556 | + |
| 557 | + def test_oauth_token_exchange_invalid_redirect_uri |
| 558 | + req = @test_harness.request( |
| 559 | + { |
| 560 | + ':method' => 'POST', |
| 561 | + ':path' => '/oauth/token', |
| 562 | + 'content-type' => 'application/x-www-form-urlencoded' |
| 563 | + }, |
| 564 | + URI.encode_www_form( |
| 565 | + grant_type: 'authorization_code', |
| 566 | + code: @auth_code, |
| 567 | + redirect_uri: 'http://localhost:4321/foo', |
| 568 | + client_id: @client_id, |
| 569 | + code_verifier: @code_verifier |
| 570 | + ) |
| 571 | + ) |
| 572 | + |
| 573 | + assert_equal HTTP::BAD_REQUEST, req.response_status |
| 574 | + assert_equal 'application/json', req.response_content_type |
| 575 | + json = req.response_json |
| 576 | + |
| 577 | + error = json['error'] |
| 578 | + assert_equal 'invalid_request', error |
| 579 | + end |
| 580 | + |
| 581 | + def test_oauth_token_exchange_invalid_client_id |
| 582 | + req = @test_harness.request( |
| 583 | + { |
| 584 | + ':method' => 'POST', |
| 585 | + ':path' => '/oauth/token', |
| 586 | + 'content-type' => 'application/x-www-form-urlencoded' |
| 587 | + }, |
| 588 | + URI.encode_www_form( |
| 589 | + grant_type: 'authorization_code', |
| 590 | + code: @auth_code + '!', |
| 591 | + redirect_uri: 'http://localhost:4321/callback', |
| 592 | + client_id: @client_id + 'foo', |
| 593 | + code_verifier: @code_verifier |
| 594 | + ) |
| 595 | + ) |
| 596 | + |
| 597 | + assert_equal HTTP::BAD_REQUEST, req.response_status |
| 598 | + assert_equal 'application/json', req.response_content_type |
| 599 | + json = req.response_json |
| 600 | + |
| 601 | + error = json['error'] |
| 602 | + assert_equal 'invalid_request', error |
| 603 | + end |
| 604 | + |
| 605 | + def test_oauth_token_exchange_invalid_code_verifier |
| 606 | + req = @test_harness.request( |
| 607 | + { |
| 608 | + ':method' => 'POST', |
| 609 | + ':path' => '/oauth/token', |
| 610 | + 'content-type' => 'application/x-www-form-urlencoded' |
| 611 | + }, |
| 612 | + URI.encode_www_form( |
| 613 | + grant_type: 'authorization_code', |
| 614 | + code: @auth_code + '!', |
| 615 | + redirect_uri: 'http://localhost:4321/callback', |
| 616 | + client_id: @client_id, |
| 617 | + code_verifier: @code_verifier + 'abc' |
| 618 | + ) |
| 619 | + ) |
| 620 | + |
| 621 | + assert_equal HTTP::BAD_REQUEST, req.response_status |
| 622 | + assert_equal 'application/json', req.response_content_type |
| 623 | + json = req.response_json |
| 624 | + |
| 625 | + error = json['error'] |
| 626 | + assert_equal 'invalid_request', error |
| 627 | + end |
| 628 | +end |
0 commit comments