{{ 'noconsent_error' |trans }}
6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | array( 8 | 'authoauth2:OAuth2', 9 | // *** Required for all integrations *** 10 | 'urlAuthorize' => 'https://www.facebook.com/dialog/oauth', 11 | 'urlAccessToken' => 'https://graph.facebook.com/oauth/access_token', 12 | 'urlResourceOwnerDetails' => 'https://graph.facebook.com/me?fields=id,name,first_name,last_name,email', 13 | // *** Required for facebook *** 14 | // Test App 15 | 'clientId' => '133972730583345', 16 | 'clientSecret' => '36aefb235314bad5df075363b79cbbcd', 17 | // *** Optional *** 18 | // Custom query parameters to add to authorize request 19 | 'urlAuthorizeOptions' => [ 20 | 'auth_type' => 'reauthenticate', 21 | // request email access 22 | 'req_perm' => 'email', 23 | ], 24 | ), 25 | 26 | 'genericAmazonTest' => array( 27 | 'authoauth2:OAuth2', 28 | // *** Required for all*** 29 | 'urlAuthorize' => 'https://www.amazon.com/ap/oa', 30 | 'urlAccessToken' => 'https://api.amazon.com/auth/o2/token', 31 | 'urlResourceOwnerDetails' => 'https://api.amazon.com/user/profile', 32 | // *** required for amazon *** 33 | // Test App. 34 | 'clientId' => 'amzn1.application-oa2-client.94d04152358d4f989473fecdf8553e25', 35 | 'clientSecret' => '8681bdd290df87fe1eea2d821d7dadc39fd4f89e599dfaa8a50c5656aae16980', 36 | 'scopes' => 'profile', 37 | // *** Optional *** 38 | // Allow changing the default redirectUri 39 | 'redirectUri' => 'https://abc.tutorial.stack-dev.cirrusidentity.com:8732/module.php/authoauth2/linkback', 40 | ), 41 | 42 | 'genericGoogleTest' => array( 43 | 'authoauth2:OAuth2', 44 | // *** Required for all*** 45 | 'urlAuthorize' => 'https://accounts.google.com/o/oauth2/auth', 46 | 'urlAccessToken' => 'https://accounts.google.com/o/oauth2/token', 47 | 'urlResourceOwnerDetails' => 'https://www.googleapis.com/plus/v1/people/me/openIdConnect', 48 | // userinfo doesn't need need Google Plus API access 49 | // 'urlResourceOwnerDetails' => 'https://www.googleapis.com/oauth2/v3/userinfo', 50 | //'urlResourceOwnerDetails' => 'https://www.googleapis.com/plus/v1/people/me?fields=id,name', 51 | // *** required for google *** 52 | // Test App. 53 | 'clientId' => '685947170891-0fcfnkkt6q0veqhvlpbr7a98i29p8rlf.apps.googleusercontent.com', 54 | 'clientSecret' => 'wV0FdFs_KEF1oY7XcBGq2TzM', 55 | 'scopes' => array( 56 | 'openid', 57 | 'email', 58 | 'profile' 59 | ), 60 | 'scopeSeparator' => ' ', 61 | // *** Optional *** 62 | // Allow changing the default redirectUri 63 | ), 64 | 65 | 'googleTempate' => [ 66 | 'authoauth2:OAuth2', 67 | 'template' => 'GoogleOIDC', 68 | // Client with Google Plus API access disabled 69 | 'clientId' => '815042564757-2ek814rm61bjtih4tpar8qh0pkrciifc.apps.googleusercontent.com', 70 | 'clientSecret' => 'eyM-J6cOa3FlhIeKtyd4nDX9' 71 | ], 72 | 73 | 'googleTest' => array( 74 | // Must install correct provider with: composer require league/oauth2-google 75 | 'authoauth2:OAuth2', 76 | 'providerClass' => 'League\OAuth2\Client\Provider\Google', 77 | // *** required for google *** 78 | // Test App with Google Plus access 79 | 'clientId' => '685947170891-0fcfnkkt6q0veqhvlpbr7a98i29p8rlf.apps.googleusercontent.com', 80 | 'clientSecret' => 'wV0FdFs_KEF1oY7XcBGq2TzM', 81 | ), 82 | //OpenID Connect provider https://accounts.google.com 83 | 'https://accounts.google.com' => array( 84 | 'authoauth2:OpenIDConnect', 85 | 86 | // Scopes to request, should include openid 87 | 'scopes' => ['openid', 'profile'], 88 | 89 | // Configured client id and secret 90 | 'clientId' => '685947170891-0fcfnkkt6q0veqhvlpbr7a98i29p8rlf.apps.googleusercontent.com', 91 | 'clientSecret' => 'wV0FdFs_KEF1oY7XcBGq2TzM', 92 | 93 | 'scopeSeparator' => ' ', 94 | 'issuer' => 'https://accounts.google.com', 95 | 'urlAuthorize' => 'https://accounts.google.com/o/oauth2/v2/auth', 96 | 'urlAccessToken' => 'https://oauth2.googleapis.com/token', 97 | 'urlResourceOwnerDetails' => 'https://openidconnect.googleapis.com/v1/userinfo', 98 | 'keys' => array ( 99 | 'df8d9ee403bcc7185ad51041194bd3433742d9aa' => '-----BEGIN PUBLIC KEY----- 100 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnQgOafNApTMwKerFuGXD 101 | j8HZ7hUSFPUV4/SzYj79SF5giP0IfF6Ksnb5Jy0pQ/MXQ6XNuh6eZqCfAPXUwHto 102 | xE29jpe6L6DGKPLTr8RTbNhdIsorc1yXiPcail58gftq1fmegZw0KO6QtBpKYnBW 103 | oZw4PJkuP8ZdGanA0btsZRRRYVmSOKuYDNHfVJlcrD4cqAOL3BPjWQIrZszwTVmw 104 | 0FjiU9KfGtU0rDYnas+mZv1qfetZkTA3YPTqSspCNZDbGCVXpJnr4pai0E7lxFgD 105 | NDN2IDk955Pf8eG8oNCfqkHXfnWDrTlXP7SSrYmEaBPcmMKOHdjyrYPk0lWI8+ur 106 | XwIDAQAB 107 | -----END PUBLIC KEY-----', 108 | 'f6f80f37f21c23e61f2be4231e27d269d6695329' => '-----BEGIN PUBLIC KEY----- 109 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8Q+bsTm7MfrGQsnigd+0 110 | ix9EYUesUEJWGpK6jRjArdphVkE7xHqrHbIGQcFrRKOeatHDCXtBKDWTbVOJugCc 111 | 5EC8CeH+q54VU5YxunooUCK4jTQW1piLq0BpOKM0dbHxpEQtGRwA6Yu52ZKafswG 112 | 64BYo44kX0pPgi4sssUSn0dz0fIrcA8MSa8iffICPKfe757I3en7XTypKFs5BCPo 113 | PAhYHoCqrQnOoRh7ieVvAQUeiaKASjngGSo+5GWpsMzQO05+2J3vId01f0oRUTJY 114 | trKppNS8LxXr8BXSp66SBwgXZEhFLOcmnM9zZEAPt/DMd3IQZUaOF3w5h3ZUHMXc 115 | zwIDAQAB 116 | -----END PUBLIC KEY-----', 117 | ) 118 | ), 119 | 120 | // ORCID OpenID Connect Provider 121 | 'orcidOIDCTest' => array_merge(\SimpleSAML\Module\authoauth2\ConfigTemplate::OrcidOIDC, [ 122 | 'clientId' => 'APP-PRIZEPSDX1RMMI34', 123 | 'clientSecret' => '7a91a2a0-f118-447d-8401-71ba07815eb7', 124 | // *** Optional *** 125 | // Allow changing the default redirectUri 126 | 'redirectUri' => 'https://abc.tutorial.stack-dev.cirrusidentity.com:8732/module.php/authoauth2/linkback', 127 | ]), 128 | 129 | // This is a authentication source which handles admin authentication. 130 | 'admin' => array( 131 | // The default is to use core:AdminPassword, but it can be replaced with 132 | // any authentication source. 133 | 134 | 'core:AdminPassword', 135 | ), 136 | 137 | ); 138 | -------------------------------------------------------------------------------- /tests/config/config.php: -------------------------------------------------------------------------------- 1 | 'b', 22 | 'complex' => ['e' => 'f'], 23 | 'arrayValues' => ['a', 'b', 'c', 123, null], 24 | 'bool' => false, 25 | 'num' => 123, 26 | 'missing' => null, 27 | // Google plus style emails are array of objects that have key value pairs 28 | "emails" => [ 29 | 0 => [ 30 | "value" => "monitor@cirrusidentity.com", 31 | "type" => "account" 32 | ], 33 | ], 34 | ]; 35 | 36 | $attributeManipulator = new AttributeManipulator(); 37 | $flattenAttributes = $attributeManipulator->prefixAndFlatten($attributes); 38 | // Single values always become arrays and complex objects are flattened, and not strings are stringified 39 | $expectedAttributes = [ 40 | 'a' => ['b'], 41 | 'complex.e' => ['f'], 42 | 'arrayValues' => ['a', 'b', 'c', '123'], 43 | 'bool' => ['false'], 44 | 'num' => ['123'], 45 | 'emails.0.value' => ['monitor@cirrusidentity.com'], 46 | 'emails.0.type' => ['account'], 47 | ]; 48 | $this->assertEquals($expectedAttributes, $flattenAttributes); 49 | 50 | $this->assertEquals($expectedAttributes, (new Attributes())->normalizeAttributesArray($flattenAttributes)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/lib/Auth/Source/LinkedInV2AuthTest.php: -------------------------------------------------------------------------------- 1 | 'linked'], []); 34 | $attributes = $linkedInAuth->convertResourceOwnerAttributes($userAttributes, 'linkedin.'); 35 | $this->assertEquals($expectedAttributes, $attributes); 36 | } 37 | 38 | public static function attributeConversionProvider(): array 39 | { 40 | return [ 41 | [['id' => 'abc'], ['linkedin.id' => ['abc']]], 42 | [ 43 | [ 44 | 'id' => 'abc', 45 | 'firstName' => ['localized' => ['en_US' => 'Jon', 'en_CA' => 'John']], 46 | 'lastName' => ['not-used'] 47 | ], 48 | ['linkedin.id' => ['abc'], 'linkedin.firstName' => ['Jon']] 49 | ], 50 | [ 51 | [ 52 | 'id' => 'abc', 53 | 'firstName' => ['localized' => ['en_US' => 'Jon', 'en_CA' => 'John']], 54 | 'lastName' => ['localized' => ['en_CA' => 'Smith']], 55 | ], 56 | ['linkedin.id' => ['abc'], 'linkedin.firstName' => ['Jon'], 'linkedin.lastName' => ['Smith']] 57 | ], 58 | ]; 59 | } 60 | 61 | public function testNoEmailCallIfNotRequested(): void 62 | { 63 | $linkedInAuth = new LinkedInV2Auth(['AuthId' => 'linked'], ['scopes' => ['r_liteprofile']]); 64 | $state = []; 65 | /** @var AbstractProvider $mock */ 66 | /** @psalm-suppress MixedMethodCall */ 67 | $mock = $this->getMockBuilder(AbstractProvider::class) 68 | ->disableOriginalConstructor() 69 | ->getMock(); 70 | $mock->expects($this->never()) 71 | ->method('getAuthenticatedRequest'); 72 | $linkedInAuth->postFinalStep(new AccessToken(['access_token' => 'abc']), $mock, $state); 73 | 74 | $this->assertEquals([], $state, "State array not changed"); 75 | } 76 | 77 | /** 78 | * @param array $emailResponse The response from the email endpoint 79 | * @param array $expectedAttributes What the SSP attributes are expected to be 80 | * 81 | * @throws Exception 82 | */ 83 | #[DataProvider('getEmailProvider')] 84 | public function testGettingEmail(array $emailResponse, array $expectedAttributes): void 85 | { 86 | $linkedInAuth = new LinkedInV2Auth(['AuthId' => 'linked'], []); 87 | $state = [ 88 | 'Attributes' => [ 89 | 'linkedin.id' => ['abc'] 90 | ] 91 | ]; 92 | 93 | $token = new AccessToken(['access_token' => 'abc']); 94 | /** @var AbstractProvider $mock */ 95 | /** @psalm-suppress MixedMethodCall */ 96 | $mock = $this->getMockBuilder(AbstractProvider::class) 97 | ->disableOriginalConstructor() 98 | ->getMock(); 99 | 100 | $mockRequest = $this->createMock(RequestInterface::class); 101 | $mock->method('getAuthenticatedRequest') 102 | ->with('GET', 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))', $token) 103 | ->willReturn($mockRequest); 104 | 105 | $mock->method('getParsedResponse') 106 | ->with($mockRequest) 107 | ->willReturn($emailResponse); 108 | $linkedInAuth->postFinalStep($token, $mock, $state); 109 | 110 | $this->assertEquals( 111 | $expectedAttributes, 112 | $state['Attributes'], 113 | 'mail should be added' 114 | ); 115 | } 116 | 117 | public static function getEmailProvider(): array 118 | { 119 | return [ 120 | [ 121 | // valid email response 122 | [ 123 | "elements" => [ 124 | [ 125 | "handle" => "urn:li:emailAddress:5266785132", 126 | "handle~" => [ 127 | "emailAddress" => "testuser@cirrusidentity.com" 128 | ] 129 | ] 130 | ] 131 | ], 132 | // email added 133 | ['linkedin.id' => ['abc'], 'linkedin.emailAddress' => ['testuser@cirrusidentity.com']] 134 | ], 135 | [ 136 | [ 137 | 'someerror' => 'errormessage' 138 | ], 139 | // email not added 140 | ['linkedin.id' => ['abc']], 141 | ], 142 | ]; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/lib/Auth/Source/MicrosoftHybridAuthTest.php: -------------------------------------------------------------------------------- 1 | 'oauth2']; 32 | $config = [ 33 | 'template' => 'MicrosoftGraphV1', 34 | 'providerClass' => MockOAuth2Provider::class, 35 | 'authenticatedApiRequests' => ['https://mock.com/v1.0/me/memberOf'], 36 | ]; 37 | $state = [\SimpleSAML\Auth\State::ID => 'stateId']; 38 | 39 | /** @var AbstractProvider $mock */ 40 | /** @psalm-suppress MixedMethodCall */ 41 | $mock = $this->getMockBuilder(AbstractProvider::class) 42 | ->disableOriginalConstructor() 43 | ->getMock(); 44 | 45 | $token = new AccessToken(['access_token' => 'stubToken', 'id_token' => $idToken]); 46 | $mock->method('getAccessToken') 47 | ->with('authorization_code', ['code' => $code]) 48 | ->willReturn($token); 49 | 50 | // graph api seems to return null for email 51 | $attributes = ['id' => 'a76d6a7a097c1e9d', 'mail' => null]; 52 | $user = new GenericResourceOwner($attributes, 'userId'); 53 | 54 | $mock->method('getResourceOwner') 55 | ->with($token) 56 | ->willReturn($user); 57 | 58 | $mockRequest = $this->createMock(RequestInterface::class); 59 | $mock->method('getAuthenticatedRequest') 60 | ->with('GET', 'https://mock.com/v1.0/me/memberOf', $token) 61 | ->willReturn($mockRequest); 62 | 63 | $mock->method('getParsedResponse') 64 | ->with($mockRequest) 65 | ->willReturn($authenticatedRequestAttributes); 66 | 67 | MockOAuth2Provider::setDelegate($mock); 68 | 69 | // when: turning a code into a token and then into a resource owner attributes 70 | $authOAuth2 = new MicrosoftHybridAuth($info, $config); 71 | $authOAuth2->finalStep($state, $code); 72 | 73 | // then: The attributes should be returned based on the getResourceOwner call 74 | $this->assertEquals($expectedAttributes, $state['Attributes']); 75 | } 76 | 77 | 78 | public static function combineOidcAndGraphProfileProvider(): array 79 | { 80 | $expectedGraphAttributes = ['microsoft.id' => ['a76d6a7a097c1e9d'], 81 | 'microsoft.@odata.context' => ['https://graph.microsoft.com/v1.0/$metadata#directoryObjects'], 82 | 'microsoft.value.0.@odata.type' => ['#microsoft.graph.group'], 83 | 'microsoft.value.0.id' => ['11111111-1111-1111-1111-111111111111']]; 84 | // A Graph Id token. note: only the payload is valid. Header and signature are not 85 | // phpcs:ignore Generic.Files.LineLength.TooLong 86 | $validIdToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjFMVE16YWtpaGlSbGFfOHoyQkVKVlhlV01xbyJ9.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFORHBFcUNHa3lPVVFDTXpHOHRGYUUiLCJhdWQiOiI5ZTdkZTIyZS0zYTE3LTQ0ZmQtODdjNy1jNmVjZWIxYmVlMGUiLCJleHAiOjE1Mzk5NjUwNDUsImlhdCI6MTUzOTg3ODM0NSwibmJmIjoxNTM5ODc4MzQ1LCJuYW1lIjoiU3RldmUgU3RyYXR1cyIsInByZWZlcnJlZF91c2VybmFtZSI6InN0ZXZlLnN0cmF0dXNAb3V0bG9vay5jb20iLCJvaWQiOiIwMDAwMDAwMC0wMDAwLTAwMDAtYTc2ZDZhN2EwOTdjMWU5ZCIsImVtYWlsIjoic3RldmUuc3RyYXR1c0BvdXRsb29rLmNvbSIsInRpZCI6IjkxODgwNDBkLTZjNjctNGM1Yi1iMTEyMzZhMzA0YjY2ZGFkIiwiYWlvIjoiRGI1YmRMSHBaSkdla0h3czlxaHlkUkFHSGR1cSFvUDdpS1cxYzFFQkd2dWhDWnZXS2luS0FoVnFZV3NtYSEwT3ZiRTFmV1J2TUF3NHFLUVBud3N6akQwKkd2N1RsbFpOY2FxcDQ0eTM0ZyJ9.SjNeBS11Qa2eXKLhxSApShFMLQ9nDjTXT27JZm3cctM'; 87 | $authenticatedRequestAttributes = [ 88 | '@odata.context' => 'https://graph.microsoft.com/v1.0/$metadata#directoryObjects', 89 | 'value' => [ 90 | 0 => [ 91 | '@odata.type' => '#microsoft.graph.group', 92 | 'id' => '11111111-1111-1111-1111-111111111111', 93 | ], 94 | ], 95 | ]; 96 | $conflictedRequestAttributes = [ 97 | 'id' => ['11111111'], 98 | 'name' => ['Steve Stratus'], 99 | 'mail' => ['steve.stratus@outlook.com'], 100 | '@odata.context' => 'https://graph.microsoft.com/v1.0/$metadata#directoryObjects', 101 | 'value' => [ 102 | 0 => [ 103 | '@odata.type' => '#microsoft.graph.group', 104 | 'id' => '11111111-1111-1111-1111-111111111111', 105 | ], 106 | ], 107 | ]; 108 | return [ 109 | // jwt, expected attributes 110 | ['invalidJwt', $authenticatedRequestAttributes, $expectedGraphAttributes], 111 | ['', $authenticatedRequestAttributes, $expectedGraphAttributes], 112 | [null, $authenticatedRequestAttributes, $expectedGraphAttributes], 113 | ['blah.abc.egd', $authenticatedRequestAttributes, $expectedGraphAttributes], 114 | [$validIdToken, $authenticatedRequestAttributes, 115 | [ 116 | 'microsoft.name' => ['Steve Stratus'], 117 | 'microsoft.mail' => ['steve.stratus@outlook.com'], 118 | 'microsoft.id' => ['a76d6a7a097c1e9d'], 119 | 'microsoft.@odata.context' => ['https://graph.microsoft.com/v1.0/$metadata#directoryObjects'], 120 | 'microsoft.value.0.@odata.type' => ['#microsoft.graph.group'], 121 | 'microsoft.value.0.id' => ['11111111-1111-1111-1111-111111111111'], 122 | ] 123 | ], 124 | [$validIdToken, $conflictedRequestAttributes, [ 125 | 'microsoft.name' => ['Steve Stratus'], 126 | 'microsoft.mail' => ['steve.stratus@outlook.com'], 127 | 'microsoft.id' => ['11111111'], 128 | 'microsoft.@odata.context' => ['https://graph.microsoft.com/v1.0/$metadata#directoryObjects'], 129 | 'microsoft.value.0.@odata.type' => ['#microsoft.graph.group'], 130 | 'microsoft.value.0.id' => ['11111111-1111-1111-1111-111111111111'], 131 | ] 132 | ], 133 | 134 | ]; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /tests/lib/Auth/Source/OpenIDConnectTest.php: -------------------------------------------------------------------------------- 1 | self::AUTH_ID]; 27 | return new OpenIDConnect($info, $config); 28 | } 29 | 30 | public static function setUpBeforeClass(): void 31 | { 32 | // Some of the constructs in this test cause a Configuration to be created prior to us 33 | // setting the one we want to use for the test. 34 | Configuration::clearInternalState(); 35 | } 36 | 37 | public static function finalStepsDataProvider(): array 38 | { 39 | return [ 40 | [ 41 | [ 42 | 'providerClass' => MockOAuth2Provider::class, 43 | 'attributePrefix' => 'test.', 44 | 'retryOnError' => 1, 45 | 'clientId' => 'test client id', 46 | ], 47 | // phpcs:disable 48 | new AccessToken([ 49 | 'access_token' => 'stubToken', 50 | 'id_token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoidGVzdCBjbGllbnQgaWQiLCJpYXQiOjE1MTYyMzkwMjJ9.emHrAifV1IyvmTXh3lYX0oAFqqZInhDlclIlTUumut0', 51 | ]), 52 | // phpcs:enable 53 | [ 54 | 'test.name' => ['Bob'], 55 | 'test.id_token.sub' => ['1234567890'], 56 | 'test.id_token.iat' => [1516239022], 57 | 'test.id_token.aud' => ['test client id'], 58 | ], 59 | ] 60 | ]; 61 | } 62 | 63 | public static function finalStepsDataProviderWithAuthenticatedApiRequest(): array 64 | { 65 | return [ 66 | [ 67 | [ 68 | 'providerClass' => MockOAuth2Provider::class, 69 | 'attributePrefix' => 'test.', 70 | 'retryOnError' => 1, 71 | 'authenticatedApiRequests' => ['https://mock.com/v1.0/me/memberOf'], 72 | 73 | ], 74 | // phpcs:disable 75 | new AccessToken([ 76 | 'access_token' => 'stubToken', 77 | 'id_token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoidGVzdCBjbGllbnQgaWQiLCJpYXQiOjE1MTYyMzkwMjJ9.emHrAifV1IyvmTXh3lYX0oAFqqZInhDlclIlTUumut0', 78 | 79 | ]), 80 | // phpcs:enable 81 | [ 82 | 'test.name' => ['Bob'], 83 | 'test.additionalResource' => ['info'], 84 | 'test.id_token.sub' => ['1234567890'], 85 | 'test.id_token.iat' => [1516239022], 86 | 'test.id_token.aud' => ['test client id'], 87 | ], 88 | ] 89 | ]; 90 | } 91 | 92 | public static function authenticateDataProvider(): array 93 | { 94 | MockOpenIDConnectProvider::setConfig([ 95 | 'authorization_endpoint' => 'https://example.com/auth', 96 | 'token_endpoint' => 'https://example.com/token', 97 | 'userinfo_endpoint' => 'https://example.com/userinfo', 98 | 99 | ]); 100 | $config = [ 101 | 'issuer' => 'https://example.com', 102 | 'clientId' => 'test client id', 103 | 'providerClass' => MockOpenIDConnectProvider::class, 104 | ]; 105 | return [ 106 | [ 107 | $config, 108 | [ 109 | State::ID => 'stateId', 110 | 'ForceAuthn' => true, 111 | ], 112 | // phpcs:ignore Generic.Files.LineLength.TooLong 113 | 'https://example.com/auth?prompt=login&state=authoauth2%7CstateId&scope=openid%20profile&response_type=code&approval_prompt=auto&redirect_uri=http%3A%2F%2Flocalhost%2Fmodule.php%2Fauthoauth2%2Flinkback&client_id=test%20client%20id' 114 | ], 115 | [ 116 | $config, 117 | [ 118 | State::ID => 'stateId', 119 | 'isPassive' => true, 120 | ], 121 | // phpcs:ignore Generic.Files.LineLength.TooLong 122 | 'https://example.com/auth?prompt=none&state=authoauth2%7CstateId&scope=openid%20profile&response_type=code&approval_prompt=auto&redirect_uri=http%3A%2F%2Flocalhost%2Fmodule.php%2Fauthoauth2%2Flinkback&client_id=test%20client%20id' 123 | ], 124 | [ 125 | $config, 126 | [ 127 | State::ID => 'stateId', 128 | 'oidc:acr_values' => 'Level4 Level3', 129 | 'oidc:display' => 'popup', 130 | ], 131 | // phpcs:ignore Generic.Files.LineLength.TooLong 132 | 'https://example.com/auth?acr_values=Level4%20Level3&display=popup&state=authoauth2%7CstateId&scope=openid%20profile&response_type=code&approval_prompt=auto&redirect_uri=http%3A%2F%2Flocalhost%2Fmodule.php%2Fauthoauth2%2Flinkback&client_id=test%20client%20id' 133 | ], 134 | ]; 135 | } 136 | 137 | public static function authprocTokenProvider(): array 138 | { 139 | return [ 140 | [ 141 | new AccessToken([ 142 | 'access_token' => 'stubToken', 143 | //phpcs:ignore Generic.Files.LineLength.TooLong 144 | 'id_token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoidGVzdCBjbGllbnQgaWQiLCJpYXQiOjE1MTYyMzkwMjJ9.emHrAifV1IyvmTXh3lYX0oAFqqZInhDlclIlTUumut0', 145 | ]), 146 | ] 147 | ]; 148 | } 149 | 150 | public function testLogoutNoEndpointConfigured(): void 151 | { 152 | MockOpenIDConnectProvider::setConfig([ 153 | 'authorization_endpoint' => 'https://example.com/auth', 154 | 'token_endpoint' => 'https://example.com/token', 155 | 'userinfo_endpoint' => 'https://example.com/userinfo', 156 | ]); 157 | $as = $this->getInstance([ 158 | 'issuer' => 'https://example.com', 159 | 'providerClass' => MockOpenIDConnectProvider::class, 160 | ]); 161 | $state = []; 162 | $this->assertNull($as->logout($state)); 163 | } 164 | 165 | public function testLogoutNoIDTokenInState(): void 166 | { 167 | MockOpenIDConnectProvider::setConfig([ 168 | 'authorization_endpoint' => 'https://example.com/auth', 169 | 'token_endpoint' => 'https://example.com/token', 170 | 'userinfo_endpoint' => 'https://example.com/userinfo', 171 | 'end_session_endpoint' => 'https://example.org/logout', 172 | ]); 173 | $as = $this->getInstance([ 174 | 'issuer' => 'https://example.com', 175 | 'providerClass' => MockOpenIDConnectProvider::class, 176 | ]); 177 | $state = []; 178 | $this->assertNull($as->logout($state)); 179 | } 180 | 181 | public function testLogoutRedirects(): void 182 | { 183 | $expectedUrl = 'https://example.org/logout?id_token_hint=myidtoken' 184 | . '&post_logout_redirect_uri=http%3A%2F%2Flocalhost%2Fmodule.php%2Fauthoauth2%2Floggedout' 185 | . '&state=authoauth2-stateId'; 186 | // Override redirect behavior 187 | $http = $this->createMock(HTTP::class); 188 | $http->method('redirectTrustedURL') 189 | ->with($expectedUrl) 190 | ->willThrowException( 191 | new RedirectException('redirectTrustedURL', $expectedUrl) 192 | ); 193 | 194 | MockOpenIDConnectProvider::setConfig([ 195 | 'authorization_endpoint' => 'https://example.com/auth', 196 | 'token_endpoint' => 'https://example.com/token', 197 | 'userinfo_endpoint' => 'https://example.com/userinfo', 198 | 'end_session_endpoint' => 'https://example.org/logout', 199 | 200 | ]); 201 | 202 | $as = $this->getInstance([ 203 | 'issuer' => 'https://example.com', 204 | 'providerClass' => MockOpenIDConnectProvider::class, 205 | ]); 206 | $as->setHttp($http); 207 | $state = [ 208 | 'id_token' => 'myidtoken', 209 | State::ID => 'stateId', 210 | ]; 211 | try { 212 | $this->assertNull($as->logout($state)); 213 | $this->fail('Redirect expected'); 214 | } catch (RedirectException $e) { 215 | $this->assertEquals('redirectTrustedURL', $e->getMessage()); 216 | $this->assertEquals($expectedUrl, $e->getUrl()); 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /tests/lib/Auth/Source/OrcidOIDCAuthTest.php: -------------------------------------------------------------------------------- 1 | null, 28 | "email" => [], 29 | "path" => "/0000-0000-0000-0000/email" 30 | ], 31 | null 32 | ], 33 | // no primary, but one non-primary 34 | [ 35 | [ 36 | "last-modified-date" => [ 37 | "value" => 1664233445808 38 | ], 39 | "email" => [ 40 | [ 41 | "created-date" => [ 42 | "value" => 1661900382942 43 | ], 44 | "last-modified-date" => [ 45 | "value" => 1664233445808 46 | ], 47 | "source" => [ 48 | "source-orcid" => [ 49 | "uri" => "https =>//orcid.org/0000-0000-0000-0000", 50 | "path" => "0000-0000-0000-0000", 51 | "host" => "orcid.org" 52 | ], 53 | "source-client-id" => null, 54 | "source-name" => [ 55 | "value" => "Test User" 56 | ], 57 | "assertion-origin-orcid" => null, 58 | "assertion-origin-client-id" => null, 59 | "assertion-origin-name" => null 60 | ], 61 | "email" => "non-primary@example.org", 62 | "path" => null, 63 | "visibility" => "public", 64 | "verified" => true, 65 | "primary" => false, 66 | "put-code" => null 67 | ] 68 | ], 69 | "path" => "/0000-0000-0000-0000/email" 70 | ], 71 | "non-primary@example.org" 72 | ], 73 | // primary and non-primary 74 | [ 75 | [ 76 | "last-modified-date" => [ 77 | "value" => 1664233809699 78 | ], 79 | "email" => [ 80 | [ 81 | "created-date" => [ 82 | "value" => 1487980758777 83 | ], 84 | "last-modified-date" => [ 85 | "value" => 1664233809699 86 | ], 87 | "source" => [ 88 | "source-orcid" => [ 89 | "uri" => "https =>//orcid.org/0000-0000-0000-0000", 90 | "path" => "0000-0000-0000-0000", 91 | "host" => "orcid.org" 92 | ], 93 | "source-client-id" => null, 94 | "source-name" => [ 95 | "value" => "Test User" 96 | ], 97 | "assertion-origin-orcid" => null, 98 | "assertion-origin-client-id" => null, 99 | "assertion-origin-name" => null 100 | ], 101 | "email" => "non-primary@example.org", 102 | "path" => null, 103 | "visibility" => "public", 104 | "verified" => true, 105 | "primary" => false, 106 | "put-code" => null 107 | ], 108 | [ 109 | "created-date" => [ 110 | "value" => 1661900382942 111 | ], 112 | "last-modified-date" => [ 113 | "value" => 1664233445808 114 | ], 115 | "source" => [ 116 | "source-orcid" => [ 117 | "uri" => "https =>//orcid.org/0000-0000-0000-0000", 118 | "path" => "0000-0000-0000-0000", 119 | "host" => "orcid.org" 120 | ], 121 | "source-client-id" => null, 122 | "source-name" => [ 123 | "value" => "Test User" 124 | ], 125 | "assertion-origin-orcid" => null, 126 | "assertion-origin-client-id" => null, 127 | "assertion-origin-name" => null 128 | ], 129 | "email" => "primary@example.org", 130 | "path" => null, 131 | "visibility" => "public", 132 | "verified" => true, 133 | "primary" => true, 134 | "put-code" => null 135 | ] 136 | ], 137 | "path" => "/0000-0000-0000-0000/email" 138 | ], 139 | "primary@example.org" 140 | ], 141 | // only primary 142 | [ 143 | [ 144 | "last-modified-date" => [ 145 | "value" => 1664233445808 146 | ], 147 | "email" => [ 148 | [ 149 | "created-date" => [ 150 | "value" => 1661900382942 151 | ], 152 | "last-modified-date" => [ 153 | "value" => 1664233445808 154 | ], 155 | "source" => [ 156 | "source-orcid" => [ 157 | "uri" => "https =>//orcid.org/0000-0000-0000-0000", 158 | "path" => "0000-0000-0000-0000", 159 | "host" => "orcid.org" 160 | ], 161 | "source-client-id" => null, 162 | "source-name" => [ 163 | "value" => "Test User" 164 | ], 165 | "assertion-origin-orcid" => null, 166 | "assertion-origin-client-id" => null, 167 | "assertion-origin-name" => null 168 | ], 169 | "email" => "primary@example.org", 170 | "path" => null, 171 | "visibility" => "public", 172 | "verified" => true, 173 | "primary" => true, 174 | "put-code" => null 175 | ] 176 | ], 177 | "path" => "/0000-0000-0000-0000/email" 178 | ], 179 | "primary@example.org" 180 | ] 181 | ]; 182 | } 183 | 184 | /** 185 | * @param array $emailResponse The JSON response from the email endpoint 186 | * @param string|null $expectedEmail What the resolved email address should be 187 | */ 188 | #[DataProvider('emailResponseProvider')] 189 | public function testEmailResolution(array $emailResponse, ?string $expectedEmail): void 190 | { 191 | $orcidAuth = new OrcidOIDCAuth(['AuthId' => 'orcid'], []); 192 | $email = $orcidAuth->parseEmailLookupResponse($emailResponse); 193 | 194 | $this->assertEquals( 195 | $expectedEmail, 196 | $email 197 | ); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /tests/lib/Codebooks/Oauth2ErrorsEnumTest.php: -------------------------------------------------------------------------------- 1 | assertSame('access_denied', Oauth2ErrorsEnum::AccessDenied->value); 15 | $this->assertSame('consent_required', Oauth2ErrorsEnum::ConsentRequired->value); 16 | $this->assertSame('invalid_scope', Oauth2ErrorsEnum::InvalidScope->value); 17 | $this->assertSame('user_cancelled_authorize', Oauth2ErrorsEnum::UserCancelledAuthorize->value); 18 | $this->assertSame('user_cancelled_login', Oauth2ErrorsEnum::UserCancelledLogin->value); 19 | $this->assertSame('user_denied', Oauth2ErrorsEnum::UserDenied->value); 20 | } 21 | 22 | public function testEnumKeys(): void 23 | { 24 | $this->assertSame(Oauth2ErrorsEnum::AccessDenied, Oauth2ErrorsEnum::from('access_denied')); 25 | $this->assertSame(Oauth2ErrorsEnum::ConsentRequired, Oauth2ErrorsEnum::from('consent_required')); 26 | $this->assertSame(Oauth2ErrorsEnum::InvalidScope, Oauth2ErrorsEnum::from('invalid_scope')); 27 | $this->assertSame(Oauth2ErrorsEnum::UserCancelledAuthorize, Oauth2ErrorsEnum::from('user_cancelled_authorize')); 28 | $this->assertSame(Oauth2ErrorsEnum::UserCancelledLogin, Oauth2ErrorsEnum::from('user_cancelled_login')); 29 | $this->assertSame(Oauth2ErrorsEnum::UserDenied, Oauth2ErrorsEnum::from('user_denied')); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/lib/Codebooks/RoutesEnumTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('linkback', RoutesEnum::Linkback->value); 15 | } 16 | 17 | public function testLogoutEnum(): void 18 | { 19 | $this->assertEquals('logout', RoutesEnum::Logout->value); 20 | } 21 | 22 | public function testLoggedOutEnum(): void 23 | { 24 | $this->assertEquals('loggedout', RoutesEnum::LoggedOut->value); 25 | } 26 | 27 | public function testConsentErrorEnum(): void 28 | { 29 | $this->assertEquals('errors/consent', RoutesEnum::ConsentError->value); 30 | } 31 | 32 | public function testAllEnumCases(): void 33 | { 34 | $expected = [ 35 | 'Linkback' => 'linkback', 36 | 'Logout' => 'logout', 37 | 'LoggedOut' => 'loggedout', 38 | 'ConsentError' => 'errors/consent', 39 | ]; 40 | 41 | foreach (RoutesEnum::cases() as $case) { 42 | $this->assertSame($expected[$case->name], $case->value); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/lib/Controller/ErrorControllerTest.php: -------------------------------------------------------------------------------- 1 | config = Configuration::loadFromArray([ 23 | 'baseurlpath' => '/', 24 | ]); 25 | 26 | $this->controller = new ErrorController($this->config); 27 | } 28 | 29 | public function testConsent(): void 30 | { 31 | $request = Request::create('/consent', 'GET'); 32 | $response = $this->controller->consent($request); 33 | 34 | $this->assertInstanceOf(Template::class, $response); 35 | $this->assertEquals('authoauth2:errors/consent.twig', $response->getTemplateName()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/lib/Controller/OIDCLogoutControllerTest.php: -------------------------------------------------------------------------------- 1 | expectedStageState; 29 | } 30 | 31 | public function getExpectedPrefix(): string 32 | { 33 | return $this->expectedPrefix; 34 | } 35 | } 36 | 37 | // phpcs:ignore PSR1.Classes.ClassDeclaration.MultipleClasses 38 | class OIDCLogoutControllerTest extends TestCase 39 | { 40 | /** @var OIDCLogoutControllerMock */ 41 | private $controller; 42 | /** @var SourceService */ 43 | private $sourceServiceMock; 44 | /** @var \PHPUnit\Framework\MockObject\MockObject|(OAuth2&\PHPUnit\Framework\MockObject\MockObject) */ 45 | private $oauth2Mock; 46 | /** @var \PHPUnit\Framework\MockObject\MockObject|(Simple&\PHPUnit\Framework\MockObject\MockObject) */ 47 | private $simpleMock; 48 | private array $stateMock; 49 | private array $parametersMock; 50 | 51 | /** 52 | * @throws Exception 53 | */ 54 | protected function setUp(): void 55 | { 56 | $this->parametersMock = ['state' => OAuth2::STATE_PREFIX . '-statefoo']; 57 | $this->stateMock = [OAuth2::AUTHID => 'testSourceId']; 58 | 59 | // Create the mock controller 60 | $this->createControllerMock(['getSourceService', 'loadState', 'getAuthSource']); 61 | } 62 | 63 | public function testExpectedConstVariables(): void 64 | { 65 | $this->assertEquals(OpenIDConnect::STAGE_LOGOUT, $this->controller->getExpectedStageState()); 66 | $this->assertEquals(OAuth2::STATE_PREFIX . '-', $this->controller->getExpectedPrefix()); 67 | } 68 | 69 | public static function requestMethod(): array 70 | { 71 | return [ 72 | 'GET' => ['GET'], 73 | 'POST' => ['POST'], 74 | ]; 75 | } 76 | 77 | #[DataProvider('requestMethod')] 78 | public function testLoggedOutSuccess(string $requestMethod): void 79 | { 80 | $parameters = [ 81 | ...$this->parametersMock, 82 | ]; 83 | 84 | $request = Request::create( 85 | uri: 'https://localhost/auth/authorize', 86 | method: $requestMethod, 87 | parameters: $parameters, 88 | ); 89 | 90 | /** @psalm-suppress UndefinedMethod,MixedMethodCall */ 91 | $this->sourceServiceMock 92 | ->expects($this->once()) 93 | ->method('completeLogout') 94 | ->with($this->stateMock); 95 | 96 | $this->controller->loggedout($request); 97 | } 98 | 99 | #[DataProvider('requestMethod')] 100 | public function testLogoutWithoutAuthSourceThrowsBadRequest(string $requestMethod): void 101 | { 102 | $parameters = [ 103 | ...$this->parametersMock, 104 | ]; 105 | 106 | $request = Request::create( 107 | uri: 'https://localhost/auth/authorize', 108 | method: $requestMethod, 109 | parameters: $parameters, 110 | ); 111 | 112 | $this->expectException(BadRequest::class); 113 | $this->expectExceptionMessage('No authsource in the request'); 114 | 115 | $this->controller->logout($request); 116 | } 117 | 118 | #[DataProvider('requestMethod')] 119 | public function testLogoutWithInvalidAuthSourceThrowsBadRequest(string $requestMethod): void 120 | { 121 | $parameters = [ 122 | 'authSource' => ['INVALID SOURCE ID'], 123 | ...$this->parametersMock, 124 | ]; 125 | 126 | $request = Request::create( 127 | uri: 'https://localhost/auth/authorize', 128 | method: $requestMethod, 129 | parameters: $parameters, 130 | ); 131 | 132 | $this->expectException(BadRequest::class); 133 | $this->expectExceptionMessage('Authsource ID invalid'); 134 | 135 | $this->controller->logout($request); 136 | } 137 | 138 | #[DataProvider('requestMethod')] 139 | public function testSuccessfullLogout(string $requestMethod): void 140 | { 141 | $parameters = [ 142 | 'authSource' => 'authsourceid', 143 | ...$this->parametersMock, 144 | ]; 145 | 146 | $request = Request::create( 147 | uri: 'https://localhost/auth/authorize', 148 | method: $requestMethod, 149 | parameters: $parameters, 150 | ); 151 | 152 | $logoutConfig = [ 153 | 'oidc:localLogout' => true, 154 | 'ReturnTo' => '/' . RoutesEnum::Logout->value 155 | ]; 156 | 157 | /** @psalm-suppress UndefinedMethod,MixedMethodCall */ 158 | $this->simpleMock 159 | ->expects($this->once()) 160 | ->method('logout') 161 | ->with($logoutConfig); 162 | 163 | $this->controller->logout($request); 164 | } 165 | 166 | // Mock helper function 167 | private function createControllerMock(array $methods): void 168 | { 169 | $this->oauth2Mock = $this->getMockBuilder(OAuth2::class) 170 | ->disableOriginalConstructor() 171 | ->getMock(); 172 | 173 | $this->simpleMock = $this->getMockBuilder(Simple::class) 174 | ->disableOriginalConstructor() 175 | ->onlyMethods(['logout']) 176 | ->getMock(); 177 | 178 | $this->sourceServiceMock = $this->getMockBuilder(SourceService::class) 179 | ->onlyMethods(['completeLogout', 'getById']) 180 | ->getMock(); 181 | 182 | $this->controller = $this->getMockBuilder(OIDCLogoutControllerMock::class) 183 | ->onlyMethods($methods) 184 | ->getMock(); 185 | 186 | /** @psalm-suppress UndefinedMethod,MixedMethodCall */ 187 | $this->controller 188 | ->method('getSourceService') 189 | ->willReturn($this->sourceServiceMock); 190 | 191 | $this->controller 192 | ->method('getAuthSource') 193 | ->willReturn($this->simpleMock); 194 | 195 | $this->sourceServiceMock 196 | ->method('getById') 197 | ->with('testSourceId', OAuth2::class) 198 | ->willReturn($this->oauth2Mock); 199 | 200 | $this->controller 201 | ->method('loadState') 202 | ->willReturn($this->stateMock); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /tests/lib/Controller/Oauth2ControllerTest.php: -------------------------------------------------------------------------------- 1 | expectedStageState; 36 | } 37 | 38 | public function getExpectedPrefix(): string 39 | { 40 | return $this->expectedPrefix; 41 | } 42 | } 43 | 44 | // phpcs:ignore PSR1.Classes.ClassDeclaration.MultipleClasses 45 | class Oauth2ControllerTest extends TestCase 46 | { 47 | /** @var Oauth2ControllerMock */ 48 | private $controller; 49 | /** @var HTTP */ 50 | private $httpMock; 51 | /** @var \PHPUnit\Framework\MockObject\MockObject|(OAuth2&\PHPUnit\Framework\MockObject\MockObject) */ 52 | private $oauth2Mock; 53 | /** @var SourceService */ 54 | private $sourceServiceMock; 55 | private array $stateMock; 56 | private array $parametersMock; 57 | 58 | protected function setUp(): void 59 | { 60 | $this->oauth2Mock = $this->getMockBuilder(OAuth2::class) 61 | ->disableOriginalConstructor() 62 | ->onlyMethods(['finalStep', 'getConfig']) 63 | ->getMock(); 64 | $this->sourceServiceMock = $this->getMockBuilder(SourceService::class) 65 | ->onlyMethods(['getById', 'completeAuth']) 66 | ->getMock(); 67 | 68 | 69 | $this->httpMock = $this->getMockBuilder(HTTP::class)->getMock(); 70 | $this->parametersMock = ['state' => OAuth2::STATE_PREFIX . '|statefoo']; 71 | $this->stateMock = [OAuth2::AUTHID => 'testSourceId']; 72 | } 73 | 74 | public function testExpectedConstVariables(): void 75 | { 76 | $this->createControllerMock(['getSourceService', 'loadState']); 77 | $this->assertEquals(OAuth2::STAGE_INIT, $this->controller->getExpectedStageState()); 78 | $this->assertEquals(OAuth2::STATE_PREFIX . '|', $this->controller->getExpectedPrefix()); 79 | } 80 | 81 | public static function requestMethod(): array 82 | { 83 | return [ 84 | 'GET' => ['GET'], 85 | 'POST' => ['POST'], 86 | ]; 87 | } 88 | 89 | #[DataProvider('requestMethod')] 90 | public function testLinkbackValidCode(string $requestMethod): void 91 | { 92 | $this->createControllerMock(['getSourceService', 'loadState', 'getHttp']); 93 | $parameters = [ 94 | 'code' => 'validCode', 95 | ...$this->parametersMock, 96 | ]; 97 | 98 | $request = Request::create( 99 | uri: 'https://localhost/auth/authorize', 100 | method: $requestMethod, 101 | parameters: $parameters, 102 | ); 103 | 104 | $this->oauth2Mock->expects($this->once()) 105 | ->method('finalStep') 106 | ->with($this->stateMock, 'validCode'); 107 | 108 | $this->sourceServiceMock->expects($this->once()) 109 | ->method('completeAuth') 110 | ->with($this->stateMock); 111 | 112 | $this->controller->linkback($request); 113 | } 114 | 115 | #[DataProvider('requestMethod')] 116 | public function testLinkbackWithNoCode(string $requestMethod): void 117 | { 118 | $this->createControllerMock(['getSourceService', 'loadState', 'handleError']); 119 | 120 | $parameters = [ 121 | ...$this->parametersMock, 122 | ]; 123 | 124 | $request = Request::create( 125 | uri: 'https://localhost/auth/authorize', 126 | method: $requestMethod, 127 | parameters: $parameters, 128 | ); 129 | 130 | /** @psalm-suppress UndefinedMethod,MixedMethodCall */ 131 | $this->controller 132 | ->expects($this->once()) 133 | ->method('handleError'); 134 | 135 | $this->controller->linkback($request); 136 | } 137 | 138 | #[DataProvider('requestMethod')] 139 | public function testLinkbackWithIdentityProviderException(string $requestMethod): void 140 | { 141 | $this->createControllerMock(['getSourceService', 'loadState', 'getHttp']); 142 | 143 | $parameters = [ 144 | 'code' => 'validCode', 145 | ...$this->parametersMock, 146 | ]; 147 | 148 | $request = Request::create( 149 | uri: 'https://localhost/auth/authorize', 150 | method: $requestMethod, 151 | parameters: $parameters, 152 | ); 153 | 154 | $this->oauth2Mock->expects($this->once()) 155 | ->method('finalStep') 156 | ->willThrowException(new IdentityProviderException('Error Message', 0, ['body' => 'error body'])); 157 | 158 | $this->expectException(AuthSource::class); 159 | 160 | $this->controller->linkback($request); 161 | } 162 | 163 | public static function configuration(): array 164 | { 165 | return [ //datasets 166 | 'useConsentPage (GET)' => [ // dataset 0 167 | [ 168 | ['useConsentErrorPage' => true], 169 | ], 170 | 'GET' 171 | ], 172 | 'useConsentPage & legacyRoute (GET)' => [ // data set 1 173 | [ 174 | ['useConsentErrorPage' => true, 'useLegacyRoutes' => true], 175 | ], 176 | 'GET' 177 | ], 178 | 'useConsentPage (POST)' => [ // dataset 0 179 | [ 180 | ['useConsentErrorPage' => true], 181 | ], 182 | 'POST' 183 | ], 184 | 'useConsentPage & legacyRoute (POST)' => [ // data set 1 185 | [ 186 | ['useConsentErrorPage' => true, 'useLegacyRoutes' => true], 187 | ], 188 | 'POST' 189 | ], 190 | ]; 191 | } 192 | 193 | /** 194 | * @throws Exception 195 | */ 196 | #[DataProvider('configuration')] 197 | public function testHandleErrorWithConsentedError(array $configuration, string $requestMethod): void 198 | { 199 | $this->createControllerMock(['getSourceService', 'loadState', 'getHttp']); 200 | 201 | $request = Request::create( 202 | uri: 'https://localhost/auth/authorize', 203 | method: $requestMethod, 204 | parameters: [ 205 | ...$this->parametersMock, 206 | 'error' => 'invalid_scope', 207 | 'error_description' => 'Invalid scope', 208 | ], 209 | ); 210 | 211 | $this->oauth2Mock 212 | ->method('getConfig') 213 | ->willReturn(new Configuration($configuration, 'test')); 214 | 215 | $this->controller->method('getHttp')->willReturn($this->httpMock); 216 | 217 | $this->httpMock->expects($this->once()) 218 | ->method('redirectTrustedURL') 219 | ->with('http://localhost/module.php/authoauth2/errors/consent'); 220 | 221 | $this->controller->linkback($request); 222 | } 223 | 224 | public static function oauth2errors(): array 225 | { 226 | return [ 227 | 'oauth2 valid error code (GET)' => [ 228 | [ 229 | 'error' => 'invalid_scope', 230 | 'error_description' => 'Invalid scope' 231 | ], 232 | UserAborted::class, 233 | 'GET' 234 | ], 235 | 'oauth2 invalid error code (GET)' => [ 236 | [ 237 | 'error' => 'invalid_error', 238 | 'error_description' => 'Invalid error' 239 | ], 240 | AuthSource::class, 241 | 'GET' 242 | ], 243 | 'oauth2 valid error code (POST)' => [ 244 | [ 245 | 'error' => 'invalid_scope', 246 | 'error_description' => 'Invalid scope' 247 | ], 248 | UserAborted::class, 249 | 'POST' 250 | ], 251 | 'oauth2 invalid error code(POST)' => [ 252 | [ 253 | 'error' => 'invalid_error', 254 | 'error_description' => 'Invalid error' 255 | ], 256 | AuthSource::class, 257 | 'POST' 258 | ] 259 | ]; 260 | } 261 | 262 | #[DataProvider('oauth2errors')] 263 | public function testHandleErrorThrowException(array $errorResponse, string $className, string $requestMethod): void 264 | { 265 | $this->createControllerMock(['getSourceService', 'loadState', 'getHttp']); 266 | 267 | $request = Request::create( 268 | uri: 'https://localhost/auth/authorize', 269 | method: $requestMethod, 270 | parameters: [ 271 | ...$this->parametersMock, 272 | ...$errorResponse, 273 | ], 274 | ); 275 | $configArray = ['useConsentErrorPage' => false]; 276 | 277 | $this->oauth2Mock 278 | ->method('getConfig') 279 | ->willReturn(new Configuration($configArray, 'test')); 280 | 281 | $this->controller->method('getHttp')->willReturn($this->httpMock); 282 | 283 | $this->expectException($className); 284 | 285 | $this->controller->linkback($request); 286 | } 287 | 288 | protected function createControllerMock(array $methods): void 289 | { 290 | $this->controller = $this->getMockBuilder(Oauth2ControllerMock::class) 291 | ->onlyMethods($methods) 292 | ->getMock(); 293 | 294 | /** @psalm-suppress UndefinedMethod,MixedMethodCall */ 295 | $this->controller 296 | ->method('getSourceService') 297 | ->willReturn($this->sourceServiceMock); 298 | 299 | $this->sourceServiceMock 300 | ->method('getById') 301 | ->with('testSourceId', OAuth2::class) 302 | ->willReturn($this->oauth2Mock); 303 | 304 | $this->controller 305 | ->method('loadState') 306 | ->willReturn($this->stateMock); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /tests/lib/Controller/Trait/ErrorTraitTest.php: -------------------------------------------------------------------------------- 1 | parseError($request); 23 | $this->assertEquals(['', ''], $result); 24 | } 25 | 26 | public function testParseErrorWithErrorAndDescription(): void 27 | { 28 | $request = Request::create(uri: 'localhost', parameters: [ 29 | 'error' => 'sample_error', 30 | 'error_description' => 'This is a sample error description' 31 | ]); 32 | 33 | // Test 34 | $result = $this->parseError($request); 35 | $this->assertEquals(['sample_error', 'This is a sample error description'], $result); 36 | } 37 | 38 | 39 | public function testParseErrorWithErrorOnly(): void 40 | { 41 | $request = Request::create(uri: 'localhost', parameters: [ 42 | 'error' => 'sample_error' 43 | ]); 44 | 45 | // Test 46 | $result = $this->parseError($request); 47 | $this->assertEquals(['sample_error', ''], $result); 48 | } 49 | 50 | public function testParseErrorWithDescriptionOnly(): void 51 | { 52 | $request = Request::create(uri: 'localhost', parameters: [ 53 | 'error_description' => 'This is a sample error description' 54 | ]); 55 | 56 | // Test 57 | $result = $this->parseError($request); 58 | $this->assertEquals(['', 'This is a sample error description'], $result); 59 | } 60 | 61 | public function testWillNotParseUnrecognizedQueryParam(): void 62 | { 63 | $request = Request::create(uri: 'localhost', parameters: [ 64 | 'error2' => 'This is a sample error description' 65 | ]); 66 | 67 | // Test 68 | $result = $this->parseError($request); 69 | $this->assertEquals(['', ''], $result); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/lib/Controller/Trait/RequestTraitTest.php: -------------------------------------------------------------------------------- 1 | state = $state; 36 | } 37 | 38 | public function getSourceId(): ?string 39 | { 40 | return $this->sourceId; 41 | } 42 | } 43 | 44 | // phpcs:ignore PSR1.Classes.ClassDeclaration.MultipleClasses 45 | class RequestTraitTest extends TestCase 46 | { 47 | use RequestTrait; 48 | 49 | private Request $request; 50 | private GenericController $controller; 51 | 52 | protected function setUp(): void 53 | { 54 | parent::setUp(); 55 | $this->request = new Request(); 56 | $this->expectedStateAuthId = OAuth2::AUTHID; 57 | $this->controller = $this->getMockBuilder(GenericController::class) 58 | ->onlyMethods(['loadState', 'getSourceService']) 59 | ->getMock(); 60 | } 61 | 62 | public function testStateIsValidWithMissingState(): void 63 | { 64 | $this->assertFalse($this->controller->stateIsValid($this->request)); 65 | } 66 | 67 | public function testStateIsValidWithEmptyState(): void 68 | { 69 | $this->request->query->set('state', ''); 70 | $this->assertFalse($this->controller->stateIsValid($this->request)); 71 | } 72 | 73 | public function testStateIsValidWithValidState(): void 74 | { 75 | $this->request->query->set('state', OAuth2::STATE_PREFIX . '|example'); 76 | $this->assertTrue($this->controller->stateIsValid($this->request)); 77 | } 78 | 79 | public function testStateIsValidWithInvalidState(): void 80 | { 81 | $this->request->query->set('state', 'invalid|example'); 82 | $this->assertFalse($this->controller->stateIsValid($this->request)); 83 | } 84 | 85 | public function testParseRequestWithInvalidState(): void 86 | { 87 | $this->expectException(BadRequest::class); 88 | $this->expectExceptionMessage('An error occured'); 89 | $this->request->attributes->set('_route', 'invalid_route'); 90 | $this->controller->parseRequest($this->request); 91 | } 92 | 93 | public function testParseRequestWithEmptyState(): void 94 | { 95 | $this->request->query->set('state', ''); 96 | $this->expectException(BadRequest::class); 97 | $this->expectExceptionMessage('An error occured'); 98 | $this->request->attributes->set('_route', 'invalid_route'); 99 | $this->controller->parseRequest($this->request); 100 | } 101 | 102 | public function testParseRequestWithValidState(): void 103 | { 104 | $this->request->query->set('state', OAuth2::STATE_PREFIX . '|valid_state'); 105 | $this->request->attributes->set('_route', 'valid_route'); 106 | 107 | /** @psalm-suppress UndefinedMethod,MixedMethodCall */ 108 | $this->controller->method('loadState')->willReturn([OAuth2::AUTHID => 'test_authsource_id']); 109 | 110 | $mockSourceService = $this->getMockBuilder(SourceService::class)->getMock(); 111 | $mockSource = $this->getMockBuilder(Source::class)->disableOriginalConstructor()->getMock(); 112 | /** @psalm-suppress UndefinedMethod,MixedMethodCall */ 113 | $this->controller->method('getSourceService')->willReturn($mockSourceService); 114 | 115 | $mockSourceService->method('getById')->willReturn($mockSource); 116 | 117 | $this->controller->parseRequest($this->request); 118 | 119 | $this->assertEquals('test_authsource_id', $this->controller->getSourceId()); 120 | } 121 | 122 | public function testParseRequestWithNoStateAuthId(): void 123 | { 124 | $this->request->query->set('state', OAuth2::STATE_PREFIX . '|valid_state_with_missing_authid'); 125 | $this->request->attributes->set('_route', 'valid_route'); 126 | 127 | /** @psalm-suppress UndefinedMethod,MixedMethodCall */ 128 | $this->controller->method('loadState')->willReturn(null); 129 | 130 | $this->expectException(BadRequest::class); 131 | $this->expectExceptionMessage('No authsource id data in state for ' . OAuth2::AUTHID); 132 | 133 | $this->controller->parseRequest($this->request); 134 | } 135 | 136 | public function testParseRequestWithEmptyAuthSourceId(): void 137 | { 138 | $this->request->query->set('state', OAuth2::STATE_PREFIX . '|valid_state_with_empty_authid'); 139 | $this->request->attributes->set('_route', 'valid_route'); 140 | 141 | /** @psalm-suppress UndefinedMethod,MixedMethodCall */ 142 | $this->controller->method('loadState')->willReturn([OAuth2::AUTHID => '']); 143 | 144 | $this->expectException(BadRequest::class); 145 | $this->expectExceptionMessage('Source ID is undefined'); 146 | 147 | $this->controller->parseRequest($this->request); 148 | } 149 | 150 | public function testParseRequestWithInvalidSource(): void 151 | { 152 | $this->request->query->set('state', OAuth2::STATE_PREFIX . '|valid_state_with_invalid_source'); 153 | $this->request->attributes->set('_route', 'valid_route'); 154 | 155 | /** @psalm-suppress UndefinedMethod,MixedMethodCall */ 156 | $this->controller->method('loadState')->willReturn([OAuth2::AUTHID => 'invalid_source_id']); 157 | 158 | $mockSourceService = $this->getMockBuilder(SourceService::class)->getMock(); 159 | /** @psalm-suppress UndefinedMethod,MixedMethodCall */ 160 | $this->controller->method('getSourceService')->willReturn($mockSourceService); 161 | $mockSourceService->method('getById')->willReturn(null); 162 | 163 | $this->expectException(BadRequest::class); 164 | $this->expectExceptionMessage('Could not find authentication source with id invalid_source_id'); 165 | 166 | $this->controller->parseRequest($this->request); 167 | } 168 | 169 | public function testParsePostGetRequest(): void 170 | { 171 | $request = Request::create( 172 | uri: 'https://localhost/auth/authorize', 173 | method: 'POST', 174 | parameters: [ 'error' => 'invalid_request'], 175 | ); 176 | 177 | $expected = ['error' => 'invalid_request']; 178 | 179 | $this->parseRequestParamsSingleton($request); 180 | $this->assertEquals($expected, $this->requestParams); 181 | 182 | $request = Request::create( 183 | uri: 'https://localhost/auth/authorize', 184 | method: 'GET', 185 | parameters: [ 'error' => 'invalid_request'] 186 | ); 187 | 188 | $this->parseRequestParamsSingleton($request); 189 | $this->assertEquals($expected, $this->requestParams); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /tests/lib/MockOAuth2Provider.php: -------------------------------------------------------------------------------- 1 | 'https://mock.com/authorize', 29 | 'urlAccessToken' => 'https://mock.com/token', 30 | 'urlResourceOwnerDetails' => 'https://mock.com/userInfo', 31 | ]; 32 | parent::__construct(array_merge($options, $defaultOptions), $collaborators); 33 | } 34 | 35 | public function getAccessToken($grant, array $options = []): AccessTokenInterface|AccessToken 36 | { 37 | return self::$delegate->getAccessToken($grant, $options); 38 | } 39 | 40 | public function getResourceOwner(AccessToken $token): ResourceOwnerInterface 41 | { 42 | return self::$delegate->getResourceOwner($token); 43 | } 44 | 45 | public function getAuthenticatedRequest($method, $url, $token, array $options = []) 46 | { 47 | return self::$delegate->getAuthenticatedRequest($method, $url, $token, $options); 48 | } 49 | 50 | public static function setDelegate(AbstractProvider $delegate): void 51 | { 52 | self::$delegate = $delegate; 53 | } 54 | 55 | public function getParsedResponse(RequestInterface $request) 56 | { 57 | return self::$delegate->getParsedResponse($request); 58 | } 59 | 60 | public function setPkceCode($pkceCode): void 61 | { 62 | self::$delegate->setPkceCode($pkceCode); 63 | } 64 | 65 | public function getPkceCode(): ?string 66 | { 67 | return self::$delegate->getPkceCode(); 68 | } 69 | 70 | /** 71 | * Clear any cached internal state. 72 | */ 73 | public static function clearInternalState(): void 74 | { 75 | self::$delegate = null; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/lib/MockOpenIDConnectProvider.php: -------------------------------------------------------------------------------- 1 | 'https://www.facebook.com/dialog/oauth', 17 | 'urlAccessToken' => 'https://graph.facebook.com/oauth/access_token', 18 | 'urlResourceOwnerDetails' => 'https://graph.facebook.com/me?fields=123', 19 | ]; 20 | 21 | /** 22 | * @param array $tokenResponse 23 | * @param string $expectedQueryString 24 | * 25 | * @throws \Exception 26 | */ 27 | #[DataProvider('adjustProvider')] 28 | public function testAdjustingResourceOwnerUrl(array $tokenResponse, string $expectedQueryString): void 29 | { 30 | 31 | $token = new AccessToken($tokenResponse); 32 | $config = self::REQUIRED_PROVIDER_CONFIG + [ 33 | 'tokenFieldsToUserDetailsUrl' => [ 34 | 'uid' => 'uid', 35 | 'rename' => 'newname', 36 | 'access_token' => 'access_token' 37 | ] 38 | ]; 39 | $provider = new AdjustableGenericProvider($config); 40 | $url = $provider->getResourceOwnerDetailsUrl($token); 41 | $query = parse_url($url, PHP_URL_QUERY); 42 | $this->assertEquals($expectedQueryString, $query); 43 | } 44 | 45 | public static function adjustProvider(): array 46 | { 47 | return [ 48 | [ 49 | ['uid' => 'abc', 'rename' => '123', 'ignore' => 'ig', 'access_token' => 'secret'], 50 | 'fields=123&uid=abc&newname=123&access_token=secret' 51 | ], 52 | [['access_token' => 'secret'], 'fields=123&access_token=secret'], 53 | ]; 54 | } 55 | 56 | /** 57 | * Test only adjusting the url if configured 58 | */ 59 | public function testNoAdjustmentsToUrl(): void 60 | { 61 | $provider = new AdjustableGenericProvider(self::REQUIRED_PROVIDER_CONFIG); 62 | $token = new AccessToken(['access_token' => 'abc', 'someid' => 123]); 63 | $url = $provider->getResourceOwnerDetailsUrl($token); 64 | $this->assertEquals('https://graph.facebook.com/me?fields=123', $url); 65 | } 66 | 67 | /** 68 | * Confirm scope can be set with scopes or authoricationUrl.scope 69 | */ 70 | public function testSetScopes(): void 71 | { 72 | $provider = new AdjustableGenericProvider(self::REQUIRED_PROVIDER_CONFIG); 73 | $url = $provider->getAuthorizationUrl(); 74 | $request = Request::create($url); 75 | $this->assertFalse($request->query->has('scope'), 'no default scopes'); 76 | 77 | $url = $provider->getAuthorizationUrl(['scope' => 'otherscope']); 78 | $request = Request::create($url); 79 | $this->assertEquals('otherscope', $request->query->get('scope')); 80 | 81 | $provider = new AdjustableGenericProvider(self::REQUIRED_PROVIDER_CONFIG + ['scopes' => ['openid']]); 82 | 83 | $url = $provider->getAuthorizationUrl(); 84 | $request = Request::create($url); 85 | $this->assertEquals('openid', $request->query->get('scope')); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/lib/Providers/OpenIDConnectProviderTest.php: -------------------------------------------------------------------------------- 1 | expectException(IdentityProviderException::class); 44 | $this->expectExceptionMessage($expectedMessage); 45 | 46 | $configDir = !empty(getenv('SIMPLESAMLPHP_CONFIG_DIR')) ? (string)getenv('SIMPLESAMLPHP_CONFIG_DIR') : ''; 47 | MockOpenIDConnectProvider::setSigningKeys([ 48 | 'mykey' => file_get_contents($configDir . '/jwks-cert.pem') 49 | ]); 50 | $provider = new MockOpenIDConnectProvider([ 51 | 'issuer' => 'niceidp', 52 | 'clientId' => 'test client id', 53 | ]); 54 | $provider->verifyIDToken($idToken); 55 | } 56 | 57 | /** 58 | * Confirm scope can be set with scopes or authoricationUrl.scope 59 | */ 60 | public function testSetScopes(): void 61 | { 62 | $provider = new OpenIDConnectProvider( 63 | ['issuer' => 'https://accounts.google.com'] 64 | ); 65 | $url = $provider->getAuthorizationUrl(); 66 | $request = Request::create($url); 67 | $this->assertEquals('openid profile', $request->query->get('scope')); 68 | 69 | $url = $provider->getAuthorizationUrl(['scope' => 'otherscope']); 70 | $request = Request::create($url); 71 | $this->assertEquals('otherscope', $request->query->get('scope')); 72 | 73 | $provider = new OpenIDConnectProvider( 74 | ['issuer' => 'https://accounts.google.com', 'scopes' => ['openid']] 75 | ); 76 | $url = $provider->getAuthorizationUrl(); 77 | $request = Request::create($url); 78 | $this->assertEquals('openid', $request->query->get('scope')); 79 | } 80 | 81 | public function testConfiguringDiscoveryUrl(): void 82 | { 83 | $provider = new OpenIDConnectProvider( 84 | ['issuer' => 'https://accounts.example.com'] 85 | ); 86 | $this->assertEquals( 87 | 'https://accounts.example.com/.well-known/openid-configuration', 88 | $provider->getDiscoveryUrl() 89 | ); 90 | 91 | $provider = new OpenIDConnectProvider( 92 | [ 93 | 'issuer' => 'https://accounts.example.com', 94 | 'discoveryUrl' => 'https://otherhost.example.com/path/path2/.well-known/openid-configuration' 95 | ] 96 | ); 97 | $this->assertEquals( 98 | 'https://otherhost.example.com/path/path2/.well-known/openid-configuration', 99 | $provider->getDiscoveryUrl() 100 | ); 101 | } 102 | 103 | public function testGetPkceMethodGetsSetFromConfig(): void 104 | { 105 | $provider = new OpenIDConnectProvider( 106 | ['issuer' => 'https://accounts.example.com'] 107 | ); 108 | // make the protected getPkceMethod available 109 | $reflection = new \ReflectionClass($provider); 110 | $method = $reflection->getMethod('getPkceMethod'); 111 | $method->setAccessible(true); 112 | $this->assertNull($method->invoke($provider)); 113 | 114 | $provider = new OpenIDConnectProvider([ 115 | 'issuer' => 'https://accounts.example.com', 116 | 'pkceMethod' => 'S256' 117 | ]); 118 | // make the protected getPkceMethod available 119 | $reflection = new \ReflectionClass($provider); 120 | $method = $reflection->getMethod('getPkceMethod'); 121 | $method->setAccessible(true); 122 | $this->assertEquals('S256', $method->invoke($provider)); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tests/lib/RedirectException.php: -------------------------------------------------------------------------------- 1 | url = $url; 18 | } 19 | 20 | /** 21 | * @return string|null 22 | */ 23 | public function getUrl(): ?string 24 | { 25 | return $this->url; 26 | } 27 | } 28 | --------------------------------------------------------------------------------