Skip to content

Commit 44e7078

Browse files
feat: add login hint on authorization endpoint (#11)
* feat: add login hint on authorization endpoint * feat: set login hint based on login_hint query param * chore: update tests * chore: add test for login hint
1 parent 03d9725 commit 44e7078

File tree

4 files changed

+102
-9
lines changed

4 files changed

+102
-9
lines changed

src/Http/Controllers/LoginController.php

+18-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Exception;
88
use Illuminate\Contracts\Support\Responsable;
9+
use Illuminate\Http\Request;
910
use Jumbojett\OpenIDConnectClientException;
1011
use MinVWS\OpenIDConnectLaravel\Http\Responses\LoginResponseInterface;
1112
use MinVWS\OpenIDConnectLaravel\OpenIDConnectClient;
@@ -19,10 +20,11 @@ public function __construct(
1920
) {
2021
}
2122

22-
public function __invoke(): Responsable
23+
public function __invoke(Request $request): Responsable
2324
{
2425
// This redirects to the client and handles the redirect back
2526
try {
27+
$this->client->setLoginHint($this->getLoginHint($request));
2628
$this->client->authenticate();
2729
} catch (OpenIDConnectClientException $e) {
2830
return $this->exceptionHandler->handleExceptionWhileAuthenticate($e);
@@ -43,4 +45,19 @@ public function __invoke(): Responsable
4345
// Return the user information in a response
4446
return app(LoginResponseInterface::class, ['userInfo' => $userInfo]);
4547
}
48+
49+
/**
50+
* Get the login hint from the request.
51+
* @param Request $request
52+
* @return string|null
53+
*/
54+
protected function getLoginHint(Request $request): ?string
55+
{
56+
$loginHint = $request->query('login_hint');
57+
if (!is_string($loginHint)) {
58+
return null;
59+
}
60+
61+
return $loginHint;
62+
}
4663
}

src/OpenIDConnectClient.php

+44-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class OpenIDConnectClient extends BaseOpenIDConnectClient
2323
{
2424
protected ?JweDecryptInterface $jweDecrypter;
2525
protected ?OpenIDConfiguration $openIDConfiguration;
26+
protected ?string $loginHint = null;
2627

2728
public function __construct(
2829
?string $providerUrl = null,
@@ -118,14 +119,55 @@ protected function getWellKnownConfigValue($param, $default = null): string|arra
118119
}
119120

120121
/**
121-
* Overwrite the redirect method to use Laravel's abort method.
122+
* Set login hint when redirecting to authorization endpoint.
123+
* Is used when redirecting to the authorization endpoint.
124+
* @param string|null $loginHint
125+
* @return void
126+
*/
127+
public function setLoginHint(?string $loginHint = null): void
128+
{
129+
$this->loginHint = $loginHint;
130+
}
131+
132+
/**
133+
* Overwrite the redirect method to a redirect method of Laravel.
134+
* And add login_hint when redirecting to the authorization endpoint.
122135
* Sometimes the error 'Cannot modify header information - headers already sent' was thrown.
123-
* By using Laravel's abort method, this error is prevented.
136+
* By using HttpResponseException, laravel will return the given response.
124137
* @param string $url
125138
* @return void
139+
* @throws OpenIDConnectClientException
126140
*/
127141
public function redirect($url): void
128142
{
143+
$authorizationEndpoint = $this->getAuthorizationEndpoint();
144+
if (
145+
!empty($this->loginHint)
146+
&& str_starts_with($url, $authorizationEndpoint)
147+
) {
148+
$url .= "&" . http_build_query(['login_hint' => $this->loginHint]);
149+
}
150+
129151
throw new HttpResponseException(new RedirectResponse($url));
130152
}
153+
154+
/**
155+
* Get authorization_endpoint from openid configuration.
156+
* @throws OpenIDConnectClientException
157+
*/
158+
protected function getAuthorizationEndpoint(): string
159+
{
160+
if ($this->openIDConfiguration !== null) {
161+
return $this->openIDConfiguration->authorizationEndpoint;
162+
}
163+
164+
$authorizationEndpoint = $this->getWellKnownConfigValue('authorization_endpoint');
165+
if (!is_string($authorizationEndpoint)) {
166+
throw new OpenIDConnectClientException(
167+
'No authorization endpoint found in well-known config.'
168+
);
169+
}
170+
171+
return $authorizationEndpoint;
172+
}
131173
}

tests/Feature/Http/Controllers/LoginControllerTest.php

+16
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,26 @@ public function testLoginRouteRedirectsToAuthorizeUrlOfProvider(): void
3535
->assertRedirectContains("https://provider.rdobeheer.nl/authorize")
3636
->assertRedirectContains('test-client-id');
3737
}
38+
public function testLoginRouteRedirectsToAuthorizeUrlOfProviderWithLoginHint(): void
39+
{
40+
$this->mockOpenIDConfigurationLoader();
41+
42+
config()->set('oidc.client_id', 'test-client-id');
43+
44+
$response = $this->get(route('oidc.login', ['login_hint' => 'test-login-hint']));
45+
$response
46+
->assertStatus(302)
47+
->assertRedirectContains("https://provider.rdobeheer.nl/authorize")
48+
->assertRedirectContains('test-client-id')
49+
->assertRedirectContains('login_hint=test-login-hint');
50+
}
3851

3952
public function testLoginRouteReturnsUserInfoWitchMockedClient(): void
4053
{
4154
$mockClient = Mockery::mock(OpenIDConnectClient::class);
55+
$mockClient
56+
->shouldReceive('setLoginHint')
57+
->once();
4258
$mockClient
4359
->shouldReceive('authenticate')
4460
->once();

tests/Unit/Http/Controllers/LoginControllerTest.php

+24-6
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ public function testLoginControllerCanBeCreated(): void
5050
public function testExceptionHandlerIsCalledWhenAuthenticateThrowsException(): void
5151
{
5252
$mockClient = Mockery::mock(OpenIDConnectClient::class);
53+
$mockClient
54+
->shouldReceive('setLoginHint')
55+
->once();
5356
$mockClient
5457
->shouldReceive('authenticate')
5558
->andThrow(OpenIDConnectClientException::class);
@@ -64,12 +67,15 @@ public function testExceptionHandlerIsCalledWhenAuthenticateThrowsException(): v
6467
$mockExceptionHandler,
6568
);
6669

67-
$loginController->__invoke();
70+
$loginController->__invoke(new Request());
6871
}
6972

7073
public function testExceptionHandlerIsCalledWhenRequestUserInfoDoesNotReturnAnObject(): void
7174
{
7275
$mockClient = Mockery::mock(OpenIDConnectClient::class);
76+
$mockClient
77+
->shouldReceive('setLoginHint')
78+
->once();
7379
$mockClient->shouldReceive('authenticate')->once();
7480
$mockClient
7581
->shouldReceive('requestUserInfo')
@@ -89,12 +95,15 @@ public function testExceptionHandlerIsCalledWhenRequestUserInfoDoesNotReturnAnOb
8995
$mockExceptionHandler,
9096
);
9197

92-
$loginController->__invoke();
98+
$loginController->__invoke(new Request());
9399
}
94100

95101
public function testExceptionHandlerIsCalledWhenRequestUserInfoThrowsAnException(): void
96102
{
97103
$mockClient = Mockery::mock(OpenIDConnectClient::class);
104+
$mockClient
105+
->shouldReceive('setLoginHint')
106+
->once();
98107
$mockClient->shouldReceive('authenticate')->once();
99108
$mockClient
100109
->shouldReceive('requestUserInfo')
@@ -114,12 +123,15 @@ public function testExceptionHandlerIsCalledWhenRequestUserInfoThrowsAnException
114123
$mockExceptionHandler,
115124
);
116125

117-
$loginController->__invoke();
126+
$loginController->__invoke(new Request());
118127
}
119128

120129
public function testExceptionHandlerIsCalledWhenRequestUserInfoThrowsAnJweDecryptException(): void
121130
{
122131
$mockClient = Mockery::mock(OpenIDConnectClient::class);
132+
$mockClient
133+
->shouldReceive('setLoginHint')
134+
->once();
123135
$mockClient->shouldReceive('authenticate')->once();
124136
$mockClient
125137
->shouldReceive('requestUserInfo')
@@ -139,12 +151,15 @@ public function testExceptionHandlerIsCalledWhenRequestUserInfoThrowsAnJweDecryp
139151
$mockExceptionHandler,
140152
);
141153

142-
$loginController->__invoke();
154+
$loginController->__invoke(new Request());
143155
}
144156

145157
public function testLoginResponseIsReturnedWithUserInfo(): void
146158
{
147159
$mockClient = Mockery::mock(OpenIDConnectClient::class);
160+
$mockClient
161+
->shouldReceive('setLoginHint')
162+
->once();
148163
$mockClient->shouldReceive('authenticate')->once();
149164
$mockClient
150165
->shouldReceive('requestUserInfo')
@@ -158,7 +173,7 @@ public function testLoginResponseIsReturnedWithUserInfo(): void
158173
$mockExceptionHandler,
159174
);
160175

161-
$response = $loginController->__invoke();
176+
$response = $loginController->__invoke(new Request());
162177

163178
$this->assertInstanceOf(LoginResponseInterface::class, $response);
164179
$this->assertInstanceOf(Responsable::class, $response);
@@ -167,6 +182,9 @@ public function testLoginResponseIsReturnedWithUserInfo(): void
167182
public function testUserInfoIsReturned(): void
168183
{
169184
$mockClient = Mockery::mock(OpenIDConnectClient::class);
185+
$mockClient
186+
->shouldReceive('setLoginHint')
187+
->once();
170188
$mockClient->shouldReceive('authenticate')->once();
171189
$mockClient
172190
->shouldReceive('requestUserInfo')
@@ -180,7 +198,7 @@ public function testUserInfoIsReturned(): void
180198
$mockExceptionHandler,
181199
);
182200

183-
$loginResponse = $loginController->__invoke();
201+
$loginResponse = $loginController->__invoke(new Request());
184202
$response = $loginResponse->toResponse(Mockery::mock(Request::class));
185203

186204
$this->assertSame(json_encode([

0 commit comments

Comments
 (0)