Skip to content

Commit

Permalink
Login with Custom Fields (apiato#400)
Browse files Browse the repository at this point in the history
* login users with custom attributes

* add tests for feature
  • Loading branch information
johannesschobel authored Apr 8, 2018
1 parent 1717316 commit 069e49d
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 10 deletions.
28 changes: 26 additions & 2 deletions app/Containers/Authentication/Actions/ProxyApiLoginAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,40 @@ public function run(ProxyApiLoginTransporter $data): array
'grant_type' => $data->grant_type ?? 'password',
'client_id' => $data->client_id,
'client_secret' => $data->client_password,
'username' => $data->email,
// 'username' => $data->email,
'password' => $data->password,
'scope' => $data->scope ?? '',
];

$prefix = config('authentication-container.login.prefix', '');
$allowedLoginFields = config('authentication-container.login.allowed_login_attributes', ['email' => []]);
$fields = array_keys($allowedLoginFields);

$loginUsername = null;
$loginAttribute = null;

foreach ($fields as $field)
{
$fieldname = $prefix . $field;
$loginUsername = $data->getInputByKey($fieldname);
$loginAttribute = $field;

if ($loginUsername !== null) {
break;
}
}

$requestData = array_merge($requestData,
[
'username' => $loginUsername,
]
);

$responseContent = Apiato::call('Authentication@CallOAuthServerTask', [$requestData]);

// check if user email is confirmed only if that feature is enabled.
Apiato::call('Authentication@CheckIfUserIsConfirmedTask', [],
[['loginWithCredentials' => [$requestData['username'], $requestData['password']]]]);
[['loginWithCredentials' => [$requestData['username'], $requestData['password'], $loginAttribute]]]);

$refreshCookie = Apiato::call('Authentication@MakeRefreshCookieTask', [$responseContent['refresh_token']]);

Expand Down
36 changes: 36 additions & 0 deletions app/Containers/Authentication/Configs/authentication-container.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,40 @@
// add your other clients here
],


'login' => [
/*
|--------------------------------------------------------------------------
| Prefix
|--------------------------------------------------------------------------
|
| Use this $prefix variable in order to allow for nested elements.
| For example, if your login fields are nested in "data.attributes.name / data.attributes.email"
| simply est the $prefix to "data.attributes." and you are good go to!
|
| Default: ''
|
*/
'prefix' => '',

/*
|--------------------------------------------------------------------------
| Allowed Login Attributes
|--------------------------------------------------------------------------
|
| A list of fields the user is allowed to login with.
| Thereby, the key is the fieldname, the value (array) contains additional validation parameters that are applied!
|
| The order determines the order the fields are tested to login (in case multiple fields are submitted!
|
| Default: ['email' => ['email']
|
*/
'allowed_login_attributes' => [
'email' => ['email'],
// 'name' => [],
// 'phone' => ['string', 'min:6', 'max:25'],
],
],

];
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ class ProxyApiLoginTransporter extends Transporter
'type' => 'object',
'properties' => [
'email',
// 'name',
// 'phone',

'password',
'client_id',
'client_password',
'grant_type',
'scope',
],
'required' => [
'email',
'password',
'client_id',
'client_password',
Expand Down
17 changes: 12 additions & 5 deletions app/Containers/Authentication/Tasks/CheckIfUserIsConfirmedTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,28 @@ class CheckIfUserIsConfirmedTask extends Task
public function run()
{
// is the config flag set?
if(Config::get('authentication-container.require_email_confirmation')) {
if (Config::get('authentication-container.require_email_confirmation', false)) {

if(! $this->user) {
if (! $this->user) {
throw new LoginFailedException();
}

if(! $this->user->confirmed) {
if (! $this->user->confirmed) {
throw new UserNotConfirmedException();
}
}
}

public function loginWithCredentials($email, $password)
/**
* @param string $username The username / email / whatever to be used
* @param string $password the corresponding password
* @param string $field the field to be checked against
*
* @throws LoginFailedException
*/
public function loginWithCredentials($username, $password, $field = 'email')
{
if(Auth::attempt(['email' => $email, 'password' => $password])) {
if (Auth::attempt([$field => $username, 'password' => $password])) {
$this->user = Auth::user();
}
else {
Expand Down
27 changes: 25 additions & 2 deletions app/Containers/Authentication/UI/API/Requests/LoginRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,33 @@ class LoginRequest extends Request
*/
public function rules()
{
return [
'email' => 'required|email|max:40|exists:users,email',
$prefix = config('authentication-container.login.prefix', '');

$allowedLoginFields = config('authentication-container.login.allowed_login_attributes', ['email' => []]);

$rules = [
'password' => 'required|min:3|max:30',
];

foreach ($allowedLoginFields as $key => $optionalValidators)
{
// build all other login fields together
$allOtherLoginFields = array_except($allowedLoginFields, $key);
$allOtherLoginFields = array_keys($allOtherLoginFields);
$allOtherLoginFields = preg_filter('/^/', $prefix, $allOtherLoginFields);
$allOtherLoginFields = implode(',', $allOtherLoginFields);

$validators = implode('|', $optionalValidators);

$keyname = $prefix . $key;

$rules = array_merge($rules,
[
$keyname => "required_without_all:{$allOtherLoginFields}|exists:users,{$key}|{$validators}",
]);
}

return $rules;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,137 @@ public function testClientWebAdminProxyLogin_()
}
}

/**
* @test
*/
public function testLoginWithNameAttribute_()
{
$endpoint = 'post@v1/clients/web/admin/login';

// create data to be used for creating the testing user and to be sent with the post request
$data = [
'email' => 'testing@mail.com',
'password' => 'testingpass',
'name' => 'username',
];

$user = $this->getTestingUser($data);
$this->actingAs($user, 'web');

$clientId = '100';
$clientSecret = 'XXp8x4QK7d3J9R7OVRXWrhc19XPRroHTTKIbY8XX';

// create client
DB::table('oauth_clients')->insert([
[
'id' => $clientId,
'secret' => $clientSecret,
'name' => 'Testing',
'redirect' => 'http://localhost',
'password_client' => '1',
'personal_access_client' => '0',
'revoked' => '0',
],
]);

// make the clients credentials available as env variables
Config::set('authentication-container.clients.web.admin.id', $clientId);
Config::set('authentication-container.clients.web.admin.secret', $clientSecret);

// specifically allow to login with "name" attribute
Config::set('authentication-container.login.allowed_login_attributes',
[
'email' => ['email'],
'name' => [],
]
);

// create testing oauth keys files
$publicFilePath = $this->createTestingKey('oauth-public.key');
$privateFilePath = $this->createTestingKey('oauth-private.key');

$request = [
'password' => 'testingpass',
'name' => 'username',
];

$response = $this->endpoint($endpoint)->makeCall($request);

$response->assertStatus(200);

$response->assertCookie('refreshToken');

$this->assertResponseContainKeyValue([
'token_type' => 'Bearer',
]);

$this->assertResponseContainKeys(['expires_in', 'access_token']);

// delete testing keys files if they were created for this test
if ($this->testingFilesCreated) {
unlink($publicFilePath);
unlink($privateFilePath);
}
}

/**
* @test
*/
public function testLoginWithDeviceAttribute_()
{
$endpoint = 'post@v1/clients/web/admin/login';

// create data to be used for creating the testing user and to be sent with the post request
$data = [
'email' => 'testing@mail.com',
'password' => 'testingpass',
'name' => 'username',
];

$user = $this->getTestingUser($data);
$this->actingAs($user, 'web');

$clientId = '100';
$clientSecret = 'XXp8x4QK7d3J9R7OVRXWrhc19XPRroHTTKIbY8XX';

// create client
DB::table('oauth_clients')->insert([
[
'id' => $clientId,
'secret' => $clientSecret,
'name' => 'Testing',
'redirect' => 'http://localhost',
'password_client' => '1',
'personal_access_client' => '0',
'revoked' => '0',
],
]);

// make the clients credentials available as env variables
Config::set('authentication-container.clients.web.admin.id', $clientId);
Config::set('authentication-container.clients.web.admin.secret', $clientSecret);

// create testing oauth keys files
$publicFilePath = $this->createTestingKey('oauth-public.key');
$privateFilePath = $this->createTestingKey('oauth-private.key');

$request = [
'password' => 'testingpass',
'device' => 'My Fancy Device',
];

$response = $this->endpoint($endpoint)->makeCall($request);

// we test for HTTP 400 because the user is not allowed to login via name attribute
$response->assertStatus(400);

// delete testing keys files if they were created for this test
if ($this->testingFilesCreated) {
unlink($publicFilePath);
unlink($privateFilePath);
}
}

/**
* @test
*/
Expand Down
17 changes: 17 additions & 0 deletions app/Ship/Parents/Models/UserModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,21 @@ abstract class UserModel extends AbstractUserModel
use HasApiTokens;
use HasResourceKeyTrait;

public function findForPassport($identifier)
{
$allowedLoginAttributes = config('authentication-container.login.allowed_login_attributes', ['email' => []]);
$fields = array_keys($allowedLoginAttributes);

$builder = $this;

foreach ($fields as $field)
{
$builder = $builder->orWhere($field, $identifier);
}

$builder = $builder->first();

return $builder;
}

}

0 comments on commit 069e49d

Please sign in to comment.