@@ -2471,5 +2471,311 @@ public function testRequestUserInfoSignedEncrypted()
24712471 $ this ->assertEquals ($ email , $ userData ->email );
24722472 }
24732473
2474+ public function testVerifyLogoutTokenThrowsExceptionWithoutToken ()
2475+ {
2476+ $ client = new OpenIDConnectClient (
2477+ 'https://example.org ' ,
2478+ 'fake-client-id ' ,
2479+ 'fake-client-secret ' ,
2480+ );
2481+
2482+ // Call the verifyLogoutToken method without a token
2483+ $ this ->expectException (OpenIDConnectClientException::class);
2484+ $ client ->verifyLogoutToken ();
2485+ }
2486+
2487+ public function testVerifyLogoutTokenFailsWithInvalidClaims ()
2488+ {
2489+ // Create a new RSA key pair for signing the logout token
2490+ $ private_key = JWKFactory::createRSAKey (
2491+ 4096 ,
2492+ [
2493+ 'alg ' => 'RS256 ' ,
2494+ 'use ' => 'sig '
2495+ ]
2496+ );
2497+ $ public_key = $ private_key ->toPublic ();
2498+
2499+ // Generate random values for the logout token
2500+ $ kid = bin2hex (random_bytes (6 ));
2501+ $ jti = bin2hex (random_bytes (6 ));
2502+
2503+ // Create claims for the logout token
2504+ // Missing 'sub' and 'sid' claims
2505+ $ claims = [
2506+ 'exp ' => time () + 60 ,
2507+ 'iat ' => time (),
2508+ 'iss ' => 'https://example.org ' ,
2509+ 'aud ' => 'fake-client-id ' ,
2510+ 'jti ' => $ jti ,
2511+ 'events ' => json_decode ('{"http://schemas.openid.net/event/backchannel-logout": {}} ' )
2512+ ];
2513+
2514+ // Create logout token
2515+ $ logoutToken = $ this ->signClaims ($ claims , $ private_key , 'RS256 ' , ['kid ' => $ kid ]);
2516+
2517+ // List of JWKs to be returned by the JWKS endpoint
2518+ $ jwks = [[
2519+ 'kid ' => $ kid ,
2520+ ...$ public_key ->jsonSerialize ()
2521+ ]];
2522+
2523+ // Mock the OpenIDConnectClient, only mocking the fetchURL method
2524+ $ client = $ this ->getMockBuilder (OpenIDConnectClient::class)
2525+ ->setConstructorArgs ([
2526+ 'https://example.org ' ,
2527+ 'fake-client-id ' ,
2528+ 'fake-client-secret ' ,
2529+ ])
2530+ ->onlyMethods (['fetchURL ' ])
2531+ ->getMock ();
2532+
2533+ $ client ->expects ($ this ->any ())
2534+ ->method ('fetchURL ' )
2535+ ->with ($ this ->anything ())
2536+ ->will ($ this ->returnCallback (function (string $ url , ?string $ post_body = null , array $ headers = []) use ($ jwks ) {
2537+ switch ($ url ) {
2538+ case 'https://example.org/.well-known/openid-configuration ' :
2539+ return new Response (200 , 'application/json ' , json_encode ([
2540+ 'issuer ' => 'https://example.org/ ' ,
2541+ 'authorization_endpoint ' => 'https://example.org/authorize ' ,
2542+ 'token_endpoint ' => 'https://example.org/token ' ,
2543+ 'userinfo_endpoint ' => 'https://example.org/userinfo ' ,
2544+ 'jwks_uri ' => 'https://example.org/jwks ' ,
2545+ 'response_types_supported ' => ['code ' , 'id_token ' ],
2546+ 'subject_types_supported ' => ['public ' ],
2547+ 'id_token_signing_alg_values_supported ' => ['RS256 ' ],
2548+ ]));
2549+ case 'https://example.org/jwks ' :
2550+ return new Response (200 , 'application/json ' , json_encode ([
2551+ 'keys ' => $ jwks
2552+ ]));
2553+ default :
2554+ throw new Exception ("Unexpected request: $ url " );
2555+ }
2556+ }));
2557+
2558+
2559+ // Call the verifyLogoutToken
2560+ $ _REQUEST ['logout_token ' ] = $ logoutToken ;
2561+ $ this ->assertFalse ($ client ->verifyLogoutToken ());
2562+ }
2563+
2564+ public function testVerifyLogoutTokenSigned ()
2565+ {
2566+ // Create a new RSA key pair for signing the logout token
2567+ $ private_key = JWKFactory::createRSAKey (
2568+ 4096 ,
2569+ [
2570+ 'alg ' => 'RS256 ' ,
2571+ 'use ' => 'sig '
2572+ ]
2573+ );
2574+ $ public_key = $ private_key ->toPublic ();
2575+
2576+ // Generate random values for the logout token
2577+ $ kid = bin2hex (random_bytes (6 ));
2578+ $ jti = bin2hex (random_bytes (6 ));
2579+ $ sub = $ this ->faker ->uuid ();
2580+ $ sid = $ this ->faker ->uuid ();
2581+
2582+ // Create claims for the logout token
2583+ $ claims = [
2584+ 'exp ' => time () + 60 ,
2585+ 'iat ' => time (),
2586+ 'iss ' => 'https://example.org ' ,
2587+ 'aud ' => 'fake-client-id ' ,
2588+ 'sub ' => $ sub ,
2589+ 'sid ' => $ sid ,
2590+ 'jti ' => $ jti ,
2591+ 'events ' => json_decode ('{"http://schemas.openid.net/event/backchannel-logout": {}} ' )
2592+ ];
2593+
2594+ // Create logout token
2595+ $ logoutToken = $ this ->signClaims ($ claims , $ private_key , 'RS256 ' , ['kid ' => $ kid ]);
2596+
2597+ // List of JWKs to be returned by the JWKS endpoint
2598+ $ jwks = [[
2599+ 'kid ' => $ kid ,
2600+ ...$ public_key ->jsonSerialize ()
2601+ ]];
2602+
2603+ // Mock the OpenIDConnectClient, only mocking the fetchURL method
2604+ $ client = $ this ->getMockBuilder (OpenIDConnectClient::class)
2605+ ->setConstructorArgs ([
2606+ 'https://example.org ' ,
2607+ 'fake-client-id ' ,
2608+ 'fake-client-secret ' ,
2609+ ])
2610+ ->onlyMethods (['fetchURL ' ])
2611+ ->getMock ();
2612+
2613+ $ client ->expects ($ this ->any ())
2614+ ->method ('fetchURL ' )
2615+ ->with ($ this ->anything ())
2616+ ->will ($ this ->returnCallback (function (string $ url , ?string $ post_body = null , array $ headers = []) use ($ jwks ) {
2617+ switch ($ url ) {
2618+ case 'https://example.org/.well-known/openid-configuration ' :
2619+ return new Response (200 , 'application/json ' , json_encode ([
2620+ 'issuer ' => 'https://example.org/ ' ,
2621+ 'authorization_endpoint ' => 'https://example.org/authorize ' ,
2622+ 'token_endpoint ' => 'https://example.org/token ' ,
2623+ 'userinfo_endpoint ' => 'https://example.org/userinfo ' ,
2624+ 'jwks_uri ' => 'https://example.org/jwks ' ,
2625+ 'response_types_supported ' => ['code ' , 'id_token ' ],
2626+ 'subject_types_supported ' => ['public ' ],
2627+ 'id_token_signing_alg_values_supported ' => ['RS256 ' ],
2628+ ]));
2629+ case 'https://example.org/jwks ' :
2630+ return new Response (200 , 'application/json ' , json_encode ([
2631+ 'keys ' => $ jwks
2632+ ]));
2633+ default :
2634+ throw new Exception ("Unexpected request: $ url " );
2635+ }
2636+ }));
2637+
2638+
2639+ // Call the verifyLogoutToken
2640+ $ _REQUEST ['logout_token ' ] = $ logoutToken ;
2641+ $ this ->assertTrue ($ client ->verifyLogoutToken ());
2642+
2643+ // Get claims
2644+ $ this ->assertEquals ($ sub , $ client ->getVerifiedClaims ('sub ' ));
2645+ $ this ->assertEquals ($ sid , $ client ->getVerifiedClaims ('sid ' ));
2646+ $ this ->assertEquals ($ jti , $ client ->getVerifiedClaims ('jti ' ));
2647+ $ this ->assertEquals ('https://example.org ' , $ client ->getVerifiedClaims ('iss ' ));
2648+
2649+ // Get sid
2650+ $ this ->assertEquals ($ sid , $ client ->getSidFromBackChannel ());
2651+
2652+ // Get sub
2653+ $ this ->assertEquals ($ sub , $ client ->getSubjectFromBackChannel ());
2654+
2655+ // Get jti
2656+ $ this ->assertEquals ($ jti , $ client ->getJtiFromBackChannel ());
2657+ }
2658+ public function testVerifyLogoutTokenSignedEncrypted ()
2659+ {
2660+ // Create a new RSA key pair for signing the logout token
2661+ $ private_key = JWKFactory::createRSAKey (
2662+ 4096 ,
2663+ [
2664+ 'alg ' => 'RS256 ' ,
2665+ 'use ' => 'sig '
2666+ ]
2667+ );
2668+ $ public_key = $ private_key ->toPublic ();
2669+
2670+ // Create a new RSA key pair for encrypting the user info response
2671+ $ encryption_key = JWKFactory::createRSAKey (
2672+ 4096 ,
2673+ [
2674+ 'alg ' => 'RSA-OAEP-256 ' ,
2675+ 'use ' => 'enc '
2676+ ]
2677+ );
2678+
2679+ // Generate random values for the logout token
2680+ $ kid = bin2hex (random_bytes (6 ));
2681+ $ jti = bin2hex (random_bytes (6 ));
2682+ $ sub = $ this ->faker ->uuid ();
2683+ $ sid = $ this ->faker ->uuid ();
2684+
2685+ // Create claims for the logout token
2686+ $ claims = [
2687+ 'exp ' => time () + 60 ,
2688+ 'iat ' => time (),
2689+ 'iss ' => 'https://example.org ' ,
2690+ 'aud ' => 'fake-client-id ' ,
2691+ 'sub ' => $ sub ,
2692+ 'sid ' => $ sid ,
2693+ 'jti ' => $ jti ,
2694+ 'events ' => json_decode ('{"http://schemas.openid.net/event/backchannel-logout": {}} ' )
2695+ ];
2696+
2697+ // Create logout token
2698+ $ logoutToken = $ this ->signClaims ($ claims , $ private_key , 'RS256 ' , ['kid ' => $ kid ]);
2699+
2700+ $ keyEncryptionAlgorithmManager = new AlgorithmManager ([
2701+ new RSAOAEP256 (),
2702+ ]);
2703+ $ contentEncryptionAlgorithmManager = new AlgorithmManager ([
2704+ new A128CBCHS256 (),
2705+ ]);
2706+
2707+ $ jweBuilder = new JWEBuilder (
2708+ $ keyEncryptionAlgorithmManager ,
2709+ $ contentEncryptionAlgorithmManager ,
2710+ );
2711+
2712+ $ jwe = $ jweBuilder
2713+ ->create ()
2714+ ->withPayload ($ logoutToken )
2715+ ->withSharedProtectedHeader ([
2716+ 'alg ' => 'RSA-OAEP-256 ' ,
2717+ 'enc ' => 'A128CBC-HS256 ' ,
2718+ 'cty ' => 'JWT ' ,
2719+ ])
2720+ ->addRecipient ($ encryption_key ->toPublic ())
2721+ ->build ();
2722+
2723+ $ serializer = new \Jose \Component \Encryption \Serializer \CompactSerializer ();
2724+
2725+ $ encryptedLogoutToken = $ serializer ->serialize ($ jwe , 0 );
2726+
2727+
2728+ // List of JWKs to be returned by the JWKS endpoint
2729+ $ jwks = [[
2730+ 'kid ' => $ kid ,
2731+ ...$ public_key ->jsonSerialize ()
2732+ ]];
2733+
2734+ // Mock the OpenIDConnectClient, only mocking the fetchURL method
2735+ $ client = $ this ->getMockBuilder (OpenIDConnectClient::class)
2736+ ->setConstructorArgs ([
2737+ 'https://example.org ' ,
2738+ 'fake-client-id ' ,
2739+ 'fake-client-secret ' ,
2740+ ])
2741+ ->onlyMethods (['fetchURL ' , 'handleJweResponse ' ])
2742+ ->getMock ();
2743+
2744+ $ client ->expects ($ this ->any ())
2745+ ->method ('fetchURL ' )
2746+ ->with ($ this ->anything ())
2747+ ->will ($ this ->returnCallback (function (string $ url , ?string $ post_body = null , array $ headers = []) use ($ jwks ) {
2748+ switch ($ url ) {
2749+ case 'https://example.org/.well-known/openid-configuration ' :
2750+ return new Response (200 , 'application/json ' , json_encode ([
2751+ 'issuer ' => 'https://example.org/ ' ,
2752+ 'authorization_endpoint ' => 'https://example.org/authorize ' ,
2753+ 'token_endpoint ' => 'https://example.org/token ' ,
2754+ 'userinfo_endpoint ' => 'https://example.org/userinfo ' ,
2755+ 'jwks_uri ' => 'https://example.org/jwks ' ,
2756+ 'response_types_supported ' => ['code ' , 'id_token ' ],
2757+ 'subject_types_supported ' => ['public ' ],
2758+ 'id_token_signing_alg_values_supported ' => ['RS256 ' ],
2759+ ]));
2760+ case 'https://example.org/jwks ' :
2761+ return new Response (200 , 'application/json ' , json_encode ([
2762+ 'keys ' => $ jwks
2763+ ]));
2764+ default :
2765+ throw new Exception ("Unexpected request: $ url " );
2766+ }
2767+ }));
2768+
2769+ $ client ->expects ($ this ->any ())
2770+ ->method ('handleJweResponse ' )
2771+ ->with ($ encryptedLogoutToken )
2772+ ->willReturn ($ logoutToken );
2773+
2774+
2775+ // Call the verifyLogoutToken
2776+ $ _REQUEST ['logout_token ' ] = $ encryptedLogoutToken ;
2777+ $ this ->assertTrue ($ client ->verifyLogoutToken ());
2778+ }
2779+
24742780
24752781}
0 commit comments