Social Login Feature

* Added 3rd party identity providers:
  * Facebook
  *  Google
  *  Linkedin
  *  Apple
* UI changes ( 2 steps login)
* ReactJS integration
* Webpack Update
* Meta Document update (oauth2/.well-known/openid-configuration)
* Added provider param on oauth2 flow

Depends-On: https://review.opendev.org/c/osf/openstackid/+/772531
Change-Id: I86cef9379fcd6ca5320f080e062fc2abaa36203c
This commit is contained in:
smarcet@gmail.com 2021-05-13 15:55:33 -03:00
parent 3ab448a332
commit 74f56ffb26
62 changed files with 12994 additions and 14371 deletions

View File

@ -1,17 +0,0 @@
{
"presets": [
[
"env",
{
"targets": {
"node": "current"
}
}
],
"flow",
"react"
],
"plugins": [
"transform-object-rest-spread"
]
}

View File

@ -79,4 +79,22 @@ RABBITMQ_SSL=true
RABBITMQ_SSL_CAFILE=/certs/rabbit/ca-osf.pem
RABBITMQ_SSL_LOCALCERT=/certs/rabbit/client-cert-osf.pem
RABBITMQ_SSL_LOCALKEY=/certs/rabbit/client-key-osf.pem
RABBITMQ_SSL_VERIFY_PEER=false
RABBITMQ_SSL_VERIFY_PEER=false
# 3rd party idps
FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET=
FACEBOOK_REDIRECT_URI=/auth/login/facebook/callback
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=/auth/login/google/callback
APPLE_CLIENT_ID=
APPLE_CLIENT_SECRET=
APPLE_REDIRECT_URI=/auth/login/apple/callback
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
LINKEDIN_REDIRECT_URI=/auth/login/linkedin/callback

3
.gitignore vendored
View File

@ -42,3 +42,6 @@ routes.txt
model.sql
.phpunit.result.cache
!/public/web.config
/public/assets/dist/
/public/assets/login.js
/public/assets/css/login.css

2
.nvmrc
View File

@ -1 +1 @@
v8.10.0
v12.19.0

View File

@ -1,5 +1,4 @@
<?php namespace App\Http\Controllers\Api;
/**
* Copyright 2015 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -13,10 +12,8 @@
* limitations under the License.
**/
use App\Http\Controllers\Controller;
use App\Http\Controllers\Traits\JsonResponses;
use Utils\Services\ILogService;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Request;
use Exception;
/**
* Class JsonController
* @package App\Http\Controllers
@ -30,74 +27,5 @@ abstract class JsonController extends Controller {
$this->log_service = $log_service;
}
protected function error500(Exception $ex){
$this->log_service->error($ex);
return Response::json(array( 'error' => 'server error'), 500);
}
protected function created($data='ok'){
$res = Response::json($data, 201);
//jsonp
if(Request::has('callback'))
$res->setCallback(Request::input('callback'));
return $res;
}
protected function updated($data = 'ok', $has_content = true)
{
$res = Response::json($data, $has_content ? 201 : 204);
//jsonp
if (Request::has('callback')) {
$res->setCallback(Request::input('callback'));
}
return $res;
}
protected function deleted($data='ok'){
$res = Response::json($data, 204);
//jsonp
if(Request::has('callback'))
$res->setCallback(Request::input('callback'));
return $res;
}
protected function ok($data = 'ok'){
$res = Response::json($data, 200);
//jsonp
if(Request::has('callback'))
$res->setCallback(Request::input('callback'));
return $res;
}
protected function error400($data = ['message' => 'Bad Request']){
return Response::json($data, 400);
}
protected function error404($data = array('message' => 'Entity Not Found')){
return Response::json($data, 404);
}
protected function error403($data = array('message' => 'Forbidden'))
{
return Response::json($data, 403);
}
/**
* {
"message": "Validation Failed",
"errors": [
{
"resource": "Issue",
"field": "title",
"code": "missing_field"
}
]
}
* @param $messages
* @return mixed
*/
protected function error412($messages){
return Response::json(array('error'=>'validation' , 'messages' => $messages), 412);
}
use JsonResponses;
}

View File

@ -25,7 +25,6 @@ use OAuth2\Factories\OAuth2AuthorizationRequestFactory;
use OAuth2\OAuth2Message;
use OAuth2\Repositories\IClientRepository;
use OAuth2\Services\IMementoOAuth2SerializerService;
use Sokil\IsoCodes\IsoCodesFactory;
use Exception;
/**
* Class RegisterController

View File

@ -0,0 +1,129 @@
<?php namespace App\Http\Controllers;
/**
* Copyright 2021 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\libs\Auth\SocialLoginProviders;
use App\Services\Auth\IUserService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Laravel\Socialite\Facades\Socialite;
use models\exceptions\ValidationException;
use Strategies\ILoginStrategy;
use Strategies\ILoginStrategyFactory;
use Utils\Services\IAuthService;
/**
* Class SocialLoginController
* @package App\Http\Controllers
*/
final class SocialLoginController extends Controller
{
/**
* @var IAuthService
*/
private $auth_service;
/**
* @var IUserService
*/
private $user_service;
/**
* @var ILoginStrategy
*/
private $login_strategy;
/**
* SocialLoginController constructor.
* @param IAuthService $auth_service
* @param IUserService $user_service
* @param ILoginStrategyFactory $login_strategy_factory
*/
public function __construct(
IAuthService $auth_service,
IUserService $user_service,
ILoginStrategyFactory $login_strategy_factory
){
$this->auth_service = $auth_service;
$this->user_service = $user_service;
$this->middleware(function ($request, $next) use($login_strategy_factory){
// we do it here just to ensure that user session is loaded
Log::debug(sprintf("SocialLoginController::middleware"));
$this->login_strategy = $login_strategy_factory->build();
return $next($request);
});
}
/**
* @param $provider
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function redirect($provider)
{
try {
Log::debug(sprintf("SocialLoginController::redirect provider %s", $provider));
if(!SocialLoginProviders::isSupportedProvider($provider))
throw new ValidationException(sprintf("Provider %s is not supported.", $provider));
return Socialite::driver($provider)->redirect();
}
catch (\Exception $ex){
Log::error($ex);
}
return view("auth.register_error");
}
/**
* @param $provider
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|mixed
*/
public function callback($provider)
{
try {
Log::debug(sprintf("SocialLoginController::callback provider %s", $provider));
// validate provider
if(!SocialLoginProviders::isSupportedProvider($provider))
throw new ValidationException(sprintf("Provider %s is not supported.", $provider));
$social_user = Socialite::driver($provider)->user();
// try to get user by primary email from our db
Log::debug(sprintf("SocialLoginController::callback provider %s trying to get user using email %s", $provider, $social_user->getEmail()));
$user = $this->auth_service->getUserByUsername($social_user->getEmail());
if (is_null($user)) {
Log::debug(sprintf("SocialLoginController::callback provider %s user does not exists for email %s, creating ...", $provider, $social_user->getEmail()));
// if does not exists , registered it with email verified and active
$user = $this->user_service->registerUser([
'email' => $social_user->getEmail(),
'full_name' => $social_user->getName(),
'external_pic' => $social_user->getAvatar(),
'external_id' => $social_user->getId(),
'email_verified' => true,
'active' => true,
'external_provider' => $provider
]);
}
// do login
Auth::login($user, true);
// and continue the usual flow
return $this->login_strategy->postLogin([ 'provider'=> $provider ]);
}
catch (\Exception $ex){
Log::error($ex);
}
return view("auth.register_error");
}
}

View File

@ -0,0 +1,94 @@
<?php namespace App\Http\Controllers\Traits;
/**
* Copyright 2019 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Response;
use Exception;
/**
* Trait JsonResponses
* @package App\Http\Controllers\Traits
*/
trait JsonResponses
{
protected function error500(Exception $ex){
Log::error($ex);
return Response::json(array( 'error' => 'server error'), 500);
}
protected function created($data='ok'){
$res = Response::json($data, 201);
//jsonp
if(Request::has('callback'))
$res->setCallback(Request::input('callback'));
return $res;
}
protected function updated($data = 'ok', $has_content = true)
{
$res = Response::json($data, $has_content ? 201 : 204);
//jsonp
if (Request::has('callback')) {
$res->setCallback(Request::input('callback'));
}
return $res;
}
protected function deleted($data='ok'){
$res = Response::json($data, 204);
//jsonp
if(Request::has('callback'))
$res->setCallback(Request::input('callback'));
return $res;
}
protected function ok($data = 'ok'){
$res = Response::json($data, 200);
//jsonp
if(Request::has('callback'))
$res->setCallback(Request::input('callback'));
return $res;
}
protected function error400($data = ['message' => 'Bad Request']){
return Response::json($data, 400);
}
protected function error404($data = array('message' => 'Entity Not Found')){
return Response::json($data, 404);
}
protected function error403($data = array('message' => 'Forbidden'))
{
return Response::json($data, 403);
}
/**
* {
"message": "Validation Failed",
"errors": [
{
"resource": "Issue",
"field": "title",
"code": "missing_field"
}
]
}
* @param $messages
* @return mixed
*/
protected function error412($messages){
return Response::json(array('error'=>'validation' , 'messages' => $messages), 412);
}
}

View File

@ -14,6 +14,7 @@
use App\Http\Controllers\OpenId\DiscoveryController;
use App\Http\Controllers\OpenId\OpenIdController;
use App\Http\Controllers\Traits\JsonResponses;
use App\Http\Utils\CountryList;
use Auth\Exceptions\AuthenticationException;
use Auth\Exceptions\UnverifiedEmailMemberException;
@ -25,6 +26,8 @@ use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\View;
use models\exceptions\EntityNotFoundException;
use models\exceptions\ValidationException;
use OAuth2\Repositories\IApiScopeRepository;
use OAuth2\Repositories\IClientRepository;
use OpenId\Services\IUserService;
@ -224,28 +227,70 @@ final class UserController extends OpenIdController
return $this->login_strategy->cancelLogin();
}
use JsonResponses;
/**
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function getAccount(){
try {
$email = Request::input("email", "");
if(empty($email)){
throw new ValidationException("empty email.");
}
$user = $this->auth_service->getUserByUsername($email);
if(is_null($user))
throw new EntityNotFoundException();
return $this->ok(
[
'pic' => $user->getPic(),
'full_name' => $user->getFullName()
]
);
}
catch (ValidationException $ex){
Log::warning($ex);
return $this->error412($ex->getMessages());
}
catch (EntityNotFoundException $ex){
Log::warning($ex);
return $this->error404();
}
catch (Exception $ex){
Log::error($ex);
return $this->error500($ex);
}
}
public function postLogin()
{
$max_login_attempts_2_show_captcha = $this->server_configuration_service->getConfigValue("MaxFailed.LoginAttempts.2ShowCaptcha");
$login_attempts = 0;
$username = '';
try {
$login_attempts = 0;
$username = '';
$user = null;
try
{
$data = Request::all();
if (isset($data['username']))
$data['username'] = trim($data['username']);
if (isset($data['password']))
if(isset($data['password']))
$data['password'] = trim($data['password']);
$login_attempts = intval(Request::input('login_attempts'));
// Build the validation constraint set.
$rules = array
(
$rules = [
'username' => 'required|email',
'password' => 'required',
);
if ($login_attempts >= $max_login_attempts_2_show_captcha) {
];
if ($login_attempts >= $max_login_attempts_2_show_captcha)
{
$rules['g-recaptcha-response'] = 'required|recaptcha';
}
// Create a new validator instance.
@ -255,9 +300,10 @@ final class UserController extends OpenIdController
$username = $data['username'];
$password = $data['password'];
$remember = Request::input("remember");
$remember = !is_null($remember);
if ($this->auth_service->login($username, $password, $remember)) {
if ($this->auth_service->login($username, $password, $remember))
{
return $this->login_strategy->postLogin();
}
@ -270,36 +316,56 @@ final class UserController extends OpenIdController
return $this->login_strategy->errorLogin
(
array
(
[
'max_login_attempts_2_show_captcha' => $max_login_attempts_2_show_captcha,
'login_attempts' => $login_attempts,
'username' => $username,
'error_message' => "We are sorry, your username or password does not match an existing record."
)
'login_attempts' => $login_attempts,
'error_message' => "We are sorry, your username or password does not match an existing record.",
'user_fullname' => $user->getFullName(),
'user_pic' => $user->getPic(),
'user_verified' => true,
'username' => $username,
]
);
}
// validator errors
$response_data = [
'max_login_attempts_2_show_captcha' => $max_login_attempts_2_show_captcha,
'login_attempts' => $login_attempts,
'validator' => $validator
];
if(!is_null($user)){
$response_data['user_fullname'] = $user->getFullName();
$response_data['user_pic'] = $user->getPic();
$response_data['user_verified'] = true;
}
return $this->login_strategy->errorLogin
(
array
(
'max_login_attempts_2_show_captcha' => $max_login_attempts_2_show_captcha,
'login_attempts' => $login_attempts,
'validator' => $validator
)
$response_data
);
} catch (UnverifiedEmailMemberException $ex1) {
Log::warning($ex1);
$user = $this->auth_service->getUserByUsername($username);
$response_data = [
'max_login_attempts_2_show_captcha' => $max_login_attempts_2_show_captcha,
'login_attempts' => $login_attempts,
'username' => $username,
'error_message' => $ex1->getMessage()
];
if(!is_null($user)){
$response_data['user_fullname'] = $user->getFullName();
$response_data['user_pic'] = $user->getPic();
$response_data['user_verified'] = true;
}
return $this->login_strategy->errorLogin
(
array
(
'max_login_attempts_2_show_captcha' => $max_login_attempts_2_show_captcha,
'login_attempts' => $login_attempts,
'username' => $username,
'error_message' => $ex1->getMessage()
)
$response_data
);
} catch (AuthenticationException $ex2) {
Log::warning($ex2);

View File

@ -50,6 +50,13 @@ final class EventServiceProvider extends ServiceProvider
protected $listen = [
'Illuminate\Database\Events\QueryExecuted' => [
],
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
// ... other providers
'SocialiteProviders\\Facebook\\FacebookExtendSocialite@handle',
'SocialiteProviders\\Google\\GoogleExtendSocialite@handle',
'SocialiteProviders\\Apple\\AppleExtendSocialite@handle',
'SocialiteProviders\\LinkedIn\\LinkedInExtendSocialite@handle',
],
];
/**

View File

@ -11,6 +11,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\libs\Auth\SocialLoginProviders;
use Utils\IPHelper;
use Services\IUserActionService;
use Utils\Services\IAuthService;
@ -44,15 +46,26 @@ class DefaultLoginStrategy implements ILoginStrategy
public function getLogin()
{
if (Auth::guest())
return View::make("auth.login");
return View::make("auth.login", [
'supported_providers' => SocialLoginProviders::buildSupportedProviders()
]);
return Redirect::action("UserController@getProfile");
}
public function postLogin()
public function postLogin(array $params = [])
{
$user = $this->auth_service->getCurrentUser();
$identifier = $user->getIdentifier();
$this->user_action_service->addUserAction($this->auth_service->getCurrentUser()->getId(), IPHelper::getUserIp(), IUserActionService::LoginAction);
$realm = "From Site";
if(isset($params['provider']))
$realm .= " using ".strtoupper($params['provider']);
$this->user_action_service->addUserAction
(
$this->auth_service->getCurrentUser()->getId(),
IPHelper::getUserIp(),
IUserActionService::LoginAction,
$realm
);
$default_url = URL::action("UserController@getIdentity", array("identifier" => $identifier));
return Redirect::intended($default_url);
}
@ -68,11 +81,11 @@ class DefaultLoginStrategy implements ILoginStrategy
*/
public function errorLogin(array $params)
{
$response = Redirect::action('UserController@getLogin')
->with('max_login_attempts_2_show_captcha', $params['max_login_attempts_2_show_captcha'])
->with('login_attempts', $params['login_attempts']);
if(isset($params['username']))
$response= $response->with('username', $params['username']);
$response = Redirect::action('UserController@getLogin');
foreach ($params as $key => $val)
$response = $response->with($key, $val);
if(isset($params['error_message']))
$response = $response->with('flash_notice', $params['error_message']);
if(isset($params['validator']))

View File

@ -11,9 +11,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\libs\Auth\SocialLoginProviders;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Redirect;
/**
* Class DisplayResponseUserAgentStrategy
* @package Strategies
@ -36,6 +39,12 @@ class DisplayResponseUserAgentStrategy implements IDisplayResponseStrategy
*/
public function getLoginResponse(array $data = [])
{
$provider = $data["provider"] ?? null;
if(!empty($provider)) {
return redirect()->route('social_login', ['provider' => $provider]);
}
$data['supported_providers'] = SocialLoginProviders::buildSupportedProviders();
return Response::view("auth.login", $data, 200);
}
@ -45,12 +54,11 @@ class DisplayResponseUserAgentStrategy implements IDisplayResponseStrategy
*/
public function getLoginErrorResponse(array $data = [])
{
$response = Redirect::action('UserController@getLogin')
->with('max_login_attempts_2_show_captcha', $data['max_login_attempts_2_show_captcha'])
->with('login_attempts', $data['login_attempts']);
$response = Redirect::action('UserController@getLogin');
foreach ($data as $key => $val)
$response= $response->with($key, $val);
if(isset($data['username']))
$response= $response->with('username', $data['username']);
if(isset($data['error_message']))
$response = $response->with('flash_notice', $data['error_message']);
if(isset($data['validator']))

View File

@ -11,9 +11,10 @@ interface ILoginStrategy
public function getLogin();
/**
* @param array $params
* @return mixed
*/
public function postLogin();
public function postLogin(array $params = []);
/**
* @return mixed

View File

@ -0,0 +1,23 @@
<?php namespace Strategies;
/**
* Copyright 2015 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Strategies\ILoginStrategy;
/**
* Interface ILoginStrategyFactory
* @package Strategies
*/
interface ILoginStrategyFactory
{
public function build():ILoginStrategy;
}

View File

@ -0,0 +1,112 @@
<?php namespace Strategies;
/**
* Copyright 2015 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\Services\Auth\IUserService;
use Illuminate\Support\Facades\Log;
use OAuth2\Services\IMementoOAuth2SerializerService;
use OAuth2\Services\ISecurityContextService;
use OpenId\Services\IMementoOpenIdSerializerService;
use Services\IUserActionService;
use Utils\Services\IAuthService;
/**
* Class LoginStrategyFactory
* @package Strategies
*/
final class LoginStrategyFactory implements ILoginStrategyFactory
{
/**
* @var IMementoOpenIdSerializerService
*/
private $openid_memento_service;
/**
* @var IMementoOAuth2SerializerService
*/
private $oauth2_memento_service;
/**
* @var IAuthService
*/
private $auth_service;
/**
* @var IUserService
*/
private $user_service;
/**
* @var IUserActionService
*/
private $user_action_service;
/**
* @var ISecurityContextService
*/
private $security_context_service;
/**
* LoginStrategyFactory constructor.
* @param IMementoOpenIdSerializerService $openid_memento_service
* @param IMementoOAuth2SerializerService $oauth2_memento_service
* @param IAuthService $auth_service
* @param IUserService $user_service
* @param IUserActionService $user_action_service
* @param ISecurityContextService $security_context_service
*/
public function __construct
(
IMementoOpenIdSerializerService $openid_memento_service,
IMementoOAuth2SerializerService $oauth2_memento_service,
IAuthService $auth_service,
IUserService $user_service,
IUserActionService $user_action_service,
ISecurityContextService $security_context_service)
{
$this->openid_memento_service = $openid_memento_service;
$this->oauth2_memento_service = $oauth2_memento_service;
$this->auth_service = $auth_service;
$this->user_service = $user_service;
$this->user_action_service = $user_action_service;
$this->security_context_service = $security_context_service;
}
public function build():ILoginStrategy{
$res = null;
Log::debug(sprintf("LoginStrategyFactory::build"));
if ($this->openid_memento_service->exists())
{
//openid stuff
Log::debug(sprintf("LoginStrategyFactory::build OIDC"));
return new OpenIdLoginStrategy
(
$this->openid_memento_service,
$this->user_action_service,
$this->auth_service
);
}
else if ($this->oauth2_memento_service->exists())
{
Log::debug(sprintf("LoginStrategyFactory::build OAUTH2"));
return new OAuth2LoginStrategy
(
$this->auth_service,
$this->oauth2_memento_service,
$this->user_action_service,
$this->security_context_service
);
}
//default stuff
Log::debug(sprintf("LoginStrategyFactory::build DEFAULT"));
return new DefaultLoginStrategy($this->user_action_service, $this->auth_service);
}
}

View File

@ -76,10 +76,12 @@ class OAuth2LoginStrategy extends DefaultLoginStrategy
$response_strategy = DisplayResponseStrategyFactory::build($auth_request->getDisplay());
return $response_strategy->getLoginResponse();
return $response_strategy->getLoginResponse([
'provider' => $auth_request->getProvider()
]);
}
public function postLogin()
public function postLogin(array $params = [])
{
$auth_request = OAuth2AuthorizationRequestFactory::getInstance()->build(
OAuth2Message::buildFromMemento(
@ -87,8 +89,12 @@ class OAuth2LoginStrategy extends DefaultLoginStrategy
)
);
$realm = "From ".$auth_request->getRedirectUri();
if(isset($params['provider']))
$realm .= " using ".strtoupper($params['provider']);
$this->user_action_service->addUserAction($this->auth_service->getCurrentUser()->getId(), IPHelper::getUserIp(),
IUserActionService::LoginAction, $auth_request->getRedirectUri());
IUserActionService::LoginAction, $realm);
return Redirect::action("OAuth2\OAuth2ProviderController@auth");
}

View File

@ -11,6 +11,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\libs\Auth\SocialLoginProviders;
use OpenId\OpenIdMessage;
use OpenId\OpenIdProtocol;
use OpenId\Requests\OpenIdAuthenticationRequest;
@ -62,23 +64,27 @@ final class OpenIdLoginStrategy extends DefaultLoginStrategy
} else {
$params['identity_select'] = true;
}
$params['supported_providers'] = SocialLoginProviders::buildSupportedProviders();
return View::make("auth.login", $params);
}
return Redirect::action("UserController@getProfile");
}
public function postLogin()
public function postLogin(array $params = [])
{
//go to authentication flow again
$msg = OpenIdMessage::buildFromMemento($this->memento_service->load());
$realm = "From ". $msg->getParam(OpenIdProtocol::OpenIDProtocol_Realm);
if(isset($params['provider']))
$realm .= " using ".strtoupper($params['provider']);
$this->user_action_service->addUserAction
(
$this->auth_service->getCurrentUser()->getId(),
IPHelper::getUserIp(),
IUserActionService::LoginAction,
$msg->getParam(OpenIdProtocol::OpenIDProtocol_Realm)
$realm
);
return Redirect::action("OpenId\OpenIdProviderController@endpoint");

View File

@ -48,6 +48,10 @@ final class StrategyProvider extends ServiceProvider implements DeferrableProvid
// authentication strategies
App::singleton(OAuth2ServiceCatalog::AuthenticationStrategy, \Strategies\OAuth2AuthenticationStrategy::class);
App::singleton(OpenIdServiceCatalog::AuthenticationStrategy, \Strategies\OpenIdAuthenticationStrategy::class);
// factories
App::singleton(ILoginStrategyFactory::class, LoginStrategyFactory::class);
}
public function provides()
@ -61,6 +65,7 @@ final class StrategyProvider extends ServiceProvider implements DeferrableProvid
OAuth2IndirectFragmentResponse::OAuth2IndirectFragmentResponse,
OAuth2ServiceCatalog::AuthenticationStrategy,
OpenIdServiceCatalog::AuthenticationStrategy,
ILoginStrategyFactory::class,
];
}
}

View File

@ -166,6 +166,18 @@ final class UserFactory
if(isset($payload['email_verified']) && boolval($payload['email_verified']) === true && !$user->isEmailVerified())
$user->verifyEmail();
if(isset($payload['full_name']))
$user->setFullName(trim($payload['full_name']));
if(isset($payload['external_id']))
$user->setExternalId(trim($payload['external_id']));
if(isset($payload['external_pic']))
$user->setExternalPic(trim($payload['external_pic']));
if(isset($payload['external_provider']))
$user->setExternalProvider(trim($payload['external_provider']));
return $user;
}
}

View File

@ -306,6 +306,24 @@ class User extends BaseEntity
*/
private $pic;
/**
* @ORM\Column(name="external_id", type="string")
* @var string
*/
private $external_id;
/**
* @ORM\Column(name="external_provider", type="string")
* @var string
*/
private $external_provider;
/**
* @ORM\Column(name="external_pic", type="string")
* @var string
*/
private $external_pic;
// relations
/**
@ -412,6 +430,9 @@ class User extends BaseEntity
$this->spam_type = self::SpamTypeNone;
$this->company = null;
$this->phone_number = null;
$this->external_id = null;
$this->external_provider = null;
$this->external_pic = null;
}
/**
@ -823,6 +844,9 @@ class User extends BaseEntity
if (!empty($this->pic)) {
return Storage::disk('swift')->url(sprintf("%s/%s", self::getProfilePicFolder(), $this->pic));
}
if(!empty($this->external_pic))
return $this->external_pic;
return $this->getGravatarUrl();
}
catch (\Exception $ex) {
@ -1780,4 +1804,61 @@ SQL;
$this->job_title = $job_title;
}
/**
* @return string
*/
public function getExternalProvider(): ?string
{
return $this->external_provider;
}
/**
* @param string $external_provider
*/
public function setExternalProvider(string $external_provider): void
{
$this->external_provider = $external_provider;
}
/**
* @return string
*/
public function getExternalPic(): ?string
{
return $this->external_pic;
}
/**
* @param string $external_pic
*/
public function setExternalPic(string $external_pic): void
{
$this->external_pic = $external_pic;
}
/**
* @return string
*/
public function getExternalId(): string
{
return $this->external_id;
}
/**
* @param string $external_id
*/
public function setExternalId(string $external_id): void
{
$this->external_id = $external_id;
}
/**
* @param string $full_name
*/
public function setFullName(string $full_name):void{
$name_parts = explode(" ", $full_name);
$this->first_name = $name_parts[0];
$this->last_name = $name_parts[1];
}
}

View File

@ -0,0 +1,52 @@
<?php namespace App\libs\Auth;
/**
* Copyright 2021 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/**
* Class SocialLoginProviders
* @package App\libs\Auth
*/
final class SocialLoginProviders
{
const Facebook = "facebook";
const Apple = "apple";
const LinkedIn = "linkedin";
const Google = "google";
const ValidProviders = [
self::Facebook,
self::Apple,
self::LinkedIn,
self::Google
];
/**
* @param string $provider
* @return bool
*/
public static function isSupportedProvider(string $provider):bool{
return in_array($provider, self::ValidProviders);
}
/**
* @return string[]
*/
public static function buildSupportedProviders():array{
return [
self::Facebook => "Facebook",
self::Apple => "Apple",
self::LinkedIn => "LinkedIn",
self::Google => "Google",
];
}
}

View File

@ -1,4 +1,6 @@
<?php namespace OAuth2\Discovery;
use App\libs\Auth\SocialLoginProviders;
/**
* Copyright 2015 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -254,6 +256,15 @@ final class DiscoveryDocumentBuilder
return $this;
}
/**
* @return $this
*/
public function addAvailableThirdPartyIdentityProviders(){
foreach(SocialLoginProviders::ValidProviders as $provider)
$this->addArrayValue("third_party_identity_providers", $provider);
return $this;
}
/**
* @return string
*/

View File

@ -231,6 +231,11 @@ final class OAuth2Protocol implements IOAuth2Protocol
// http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
const OAuth2Protocol_Nonce = 'nonce';
/**
* custom param - social login
*/
const OAuth2Protocol_Provider = 'provider';
/**
* Time when the End-User authentication occurred. Its value is a JSON number representing the number of seconds
* from 1970-01-01T0:0:0Z as measured in UTC until the date/time. When a max_age request is made or when auth_time
@ -1403,6 +1408,7 @@ final class OAuth2Protocol implements IOAuth2Protocol
->addDisplayValueSupported(self::OAuth2Protocol_Display_Touch)
->addDisplayValueSupported(self::OAuth2Protocol_Display_Wap)
->addDisplayValueSupported(self::OAuth2Protocol_Display_Native)
->addAvailableThirdPartyIdentityProviders()
->render();
}

View File

@ -37,6 +37,7 @@ class OAuth2AuthenticationRequest extends OAuth2AuthorizationRequest
OAuth2Protocol::OAuth2Protocol_IDTokenHint,
OAuth2Protocol::OAuth2Protocol_LoginHint,
OAuth2Protocol::OAuth2Protocol_ACRValues,
OAuth2Protocol::OAuth2Protocol_Provider,
);
/**
@ -111,6 +112,10 @@ class OAuth2AuthenticationRequest extends OAuth2AuthorizationRequest
return str_contains($this->getScope(), OAuth2Protocol::OfflineAccess_Scope);
}
public function getProvider():?string{
return $this->getParam(OAuth2Protocol::OAuth2Protocol_Provider);
}
/**
* @param OAuth2AuthorizationRequest $auth_request
*/

26
babel.config.js Normal file
View File

@ -0,0 +1,26 @@
// babel.config.js
module.exports = {
presets: [
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1",
"node":"current"
},
"useBuiltIns": "usage",
"corejs": "3.9.1"
}
],
"@babel/preset-react",
"@babel/preset-flow"
],
plugins: [
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-proposal-class-properties"
]
};

View File

@ -29,31 +29,36 @@
"php": "^7.3|^8.0",
"ext-json": "*",
"ext-pdo": "*",
"laravel/framework": "^8.0",
"laravel/helpers": "^1.4",
"laravel/tinker": "^2.5",
"laravelcollective/html": "6.2.*",
"fruitcake/laravel-cors": "^2.0",
"laravel-doctrine/orm": "1.7.*",
"laravel-doctrine/extensions": "1.4.*",
"laravel-doctrine/migrations": "2.3.*",
"beberlei/doctrineextensions": "1.3.*",
"behat/transliterator": "^1.2",
"vladimir-yuldashev/laravel-queue-rabbitmq": "v11.1.*",
"s-ichikawa/laravel-sendgrid-driver": "~3.0",
"ezyang/htmlpurifier": "v4.12.0",
"fideloper/proxy": "^4.4",
"fruitcake/laravel-cors": "^2.0",
"get-stream/stream-chat": "^1.1",
"glenscott/url-normalizer": "1.4.0",
"greggilbert/recaptcha": "dev-feature/laravel8.x",
"guzzlehttp/guzzle": "^7.0.1",
"ircmaxell/random-lib": "1.1.0",
"jenssegers/agent": "2.6.3",
"laravel-doctrine/extensions": "1.4.*",
"laravel-doctrine/migrations": "2.3.*",
"laravel-doctrine/orm": "1.7.*",
"laravel/framework": "^8.0",
"laravel/helpers": "^1.4",
"laravel/socialite": "^5.2",
"laravel/tinker": "^2.5",
"laravelcollective/html": "6.2.*",
"php-opencloud/openstack": "dev-feature/guzzle_7_x",
"phpseclib/phpseclib": "2.0.11",
"predis/predis": "v1.1.6",
"s-ichikawa/laravel-sendgrid-driver": "~3.0",
"smarcet/jose4php": "1.0.17",
"socialiteproviders/apple": "^5.0",
"socialiteproviders/facebook": "^4.1",
"socialiteproviders/google": "^4.1",
"socialiteproviders/linkedin": "^4.1",
"sokil/php-isocodes": "^3.0",
"vladimir-yuldashev/laravel-queue-rabbitmq": "v11.1.*",
"zendframework/zend-crypt": "3.3.0",
"zendframework/zend-math": "3.1.1"
},

538
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c28f890e5a6afe1c57a1e232f3be5e5c",
"content-hash": "eaa4d26a199112e2689bbd94bb8e0715",
"packages": [
{
"name": "asm89/stack-cors",
@ -3362,6 +3362,75 @@
},
"time": "2021-02-16T15:27:11+00:00"
},
{
"name": "laravel/socialite",
"version": "v5.2.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
"reference": "1960802068f81e44b2ae9793932181cf1cb91b5c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/socialite/zipball/1960802068f81e44b2ae9793932181cf1cb91b5c",
"reference": "1960802068f81e44b2ae9793932181cf1cb91b5c",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/guzzle": "^6.0|^7.0",
"illuminate/http": "^6.0|^7.0|^8.0",
"illuminate/support": "^6.0|^7.0|^8.0",
"league/oauth1-client": "^1.0",
"php": "^7.2|^8.0"
},
"require-dev": {
"illuminate/contracts": "^6.0|^7.0",
"mockery/mockery": "^1.0",
"orchestra/testbench": "^4.0|^5.0|^6.0",
"phpunit/phpunit": "^8.0|^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.x-dev"
},
"laravel": {
"providers": [
"Laravel\\Socialite\\SocialiteServiceProvider"
],
"aliases": {
"Socialite": "Laravel\\Socialite\\Facades\\Socialite"
}
}
},
"autoload": {
"psr-4": {
"Laravel\\Socialite\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.",
"homepage": "https://laravel.com",
"keywords": [
"laravel",
"oauth"
],
"support": {
"issues": "https://github.com/laravel/socialite/issues",
"source": "https://github.com/laravel/socialite"
},
"time": "2021-04-06T14:38:16+00:00"
},
{
"name": "laravel/tinker",
"version": "v2.6.1",
@ -3502,6 +3571,141 @@
},
"time": "2020-12-15T20:20:05+00:00"
},
{
"name": "lcobucci/clock",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/lcobucci/clock.git",
"reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lcobucci/clock/zipball/353d83fe2e6ae95745b16b3d911813df6a05bfb3",
"reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"infection/infection": "^0.17",
"lcobucci/coding-standard": "^6.0",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-deprecation-rules": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/php-code-coverage": "9.1.4",
"phpunit/phpunit": "9.3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Lcobucci\\Clock\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Luís Cobucci",
"email": "lcobucci@gmail.com"
}
],
"description": "Yet another clock abstraction",
"support": {
"issues": "https://github.com/lcobucci/clock/issues",
"source": "https://github.com/lcobucci/clock/tree/2.0.x"
},
"funding": [
{
"url": "https://github.com/lcobucci",
"type": "github"
},
{
"url": "https://www.patreon.com/lcobucci",
"type": "patreon"
}
],
"time": "2020-08-27T18:56:02+00:00"
},
{
"name": "lcobucci/jwt",
"version": "4.1.4",
"source": {
"type": "git",
"url": "https://github.com/lcobucci/jwt.git",
"reference": "71cf170102c8371ccd933fa4df6252086d144de6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lcobucci/jwt/zipball/71cf170102c8371ccd933fa4df6252086d144de6",
"reference": "71cf170102c8371ccd933fa4df6252086d144de6",
"shasum": ""
},
"require": {
"ext-hash": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"ext-sodium": "*",
"lcobucci/clock": "^2.0",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"infection/infection": "^0.21",
"lcobucci/coding-standard": "^6.0",
"mikey179/vfsstream": "^1.6.7",
"phpbench/phpbench": "^1.0@alpha",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-deprecation-rules": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/php-invoker": "^3.1",
"phpunit/phpunit": "^9.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Lcobucci\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Luís Cobucci",
"email": "lcobucci@gmail.com",
"role": "Developer"
}
],
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
"keywords": [
"JWS",
"jwt"
],
"support": {
"issues": "https://github.com/lcobucci/jwt/issues",
"source": "https://github.com/lcobucci/jwt/tree/4.1.4"
},
"funding": [
{
"url": "https://github.com/lcobucci",
"type": "github"
},
{
"url": "https://www.patreon.com/lcobucci",
"type": "patreon"
}
],
"time": "2021-03-23T23:53:08+00:00"
},
{
"name": "league/commonmark",
"version": "1.5.7",
@ -3754,6 +3958,81 @@
],
"time": "2021-01-18T20:58:21+00:00"
},
{
"name": "league/oauth1-client",
"version": "v1.9.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/oauth1-client.git",
"reference": "1e7e6be2dc543bf466236fb171e5b20e1b06aee6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/1e7e6be2dc543bf466236fb171e5b20e1b06aee6",
"reference": "1e7e6be2dc543bf466236fb171e5b20e1b06aee6",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-openssl": "*",
"guzzlehttp/guzzle": "^6.0|^7.0",
"php": ">=7.1||>=8.0"
},
"require-dev": {
"ext-simplexml": "*",
"friendsofphp/php-cs-fixer": "^2.17",
"mockery/mockery": "^1.3.3",
"phpstan/phpstan": "^0.12.42",
"phpunit/phpunit": "^7.5||9.5"
},
"suggest": {
"ext-simplexml": "For decoding XML-based responses."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev",
"dev-develop": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"League\\OAuth1\\Client\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ben Corlett",
"email": "bencorlett@me.com",
"homepage": "http://www.webcomm.com.au",
"role": "Developer"
}
],
"description": "OAuth 1.0 Client Library",
"keywords": [
"Authentication",
"SSO",
"authorization",
"bitbucket",
"identity",
"idp",
"oauth",
"oauth1",
"single sign on",
"trello",
"tumblr",
"twitter"
],
"support": {
"issues": "https://github.com/thephpleague/oauth1-client/issues",
"source": "https://github.com/thephpleague/oauth1-client/tree/v1.9.0"
},
"time": "2021-01-20T01:40:53+00:00"
},
{
"name": "mobiledetect/mobiledetectlib",
"version": "2.8.37",
@ -5307,6 +5586,263 @@
},
"time": "2019-07-12T23:35:09+00:00"
},
{
"name": "socialiteproviders/apple",
"version": "5.0.2",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Apple.git",
"reference": "3a626202628b27fbe8500251cd609a9062a00f9e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Apple/zipball/3a626202628b27fbe8500251cd609a9062a00f9e",
"reference": "3a626202628b27fbe8500251cd609a9062a00f9e",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-openssl": "*",
"firebase/php-jwt": "^5.2",
"lcobucci/jwt": "^4.0",
"php": "^7.4 || ^8.0",
"socialiteproviders/manager": "~4.0"
},
"suggest": {
"ahilmurugesan/socialite-apple-helper": "Automatic Apple client key generation and management."
},
"type": "library",
"autoload": {
"psr-4": {
"SocialiteProviders\\Apple\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ahilan",
"email": "ahilmurugesan@gmail.com",
"role": "Developer"
},
{
"name": "Vamsi Krishna V",
"email": "vamsi@vonectech.com",
"homepage": "https://vonectech.com/",
"role": "Farmer"
}
],
"description": "Apple OAuth2 Provider for Laravel Socialite",
"keywords": [
"apple",
"apple client key",
"apple sign in",
"client key generator",
"client key refresh",
"laravel",
"laravel apple",
"laravel socialite",
"sign in with apple",
"socialite",
"socialite apple"
],
"support": {
"source": "https://github.com/SocialiteProviders/Apple/tree/5.0.2"
},
"time": "2021-03-25T22:34:09+00:00"
},
{
"name": "socialiteproviders/facebook",
"version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Facebook.git",
"reference": "9b94a9334b5d0f61de8f5a20928d63d4d8f4e00d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Facebook/zipball/9b94a9334b5d0f61de8f5a20928d63d4d8f4e00d",
"reference": "9b94a9334b5d0f61de8f5a20928d63d4d8f4e00d",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^7.2 || ^8.0",
"socialiteproviders/manager": "~4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"SocialiteProviders\\Facebook\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oleksandr Prypkhan (Alex Wells)",
"email": "autaut03@googlemail.com"
}
],
"description": "Facebook (facebook.com) OAuth2 Provider for Laravel Socialite",
"support": {
"source": "https://github.com/SocialiteProviders/Facebook/tree/4.1.0"
},
"time": "2020-12-01T23:10:59+00:00"
},
{
"name": "socialiteproviders/google",
"version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Google-Plus.git",
"reference": "1cb8f6fb2c0dd0fc8b34e95f69865663fdf0b401"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Google-Plus/zipball/1cb8f6fb2c0dd0fc8b34e95f69865663fdf0b401",
"reference": "1cb8f6fb2c0dd0fc8b34e95f69865663fdf0b401",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^7.2 || ^8.0",
"socialiteproviders/manager": "~4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"SocialiteProviders\\Google\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "xstoop",
"email": "myenglishnameisx@gmail.com"
}
],
"description": "Google OAuth2 Provider for Laravel Socialite",
"support": {
"source": "https://github.com/SocialiteProviders/Google-Plus/tree/4.1.0"
},
"time": "2020-12-01T23:10:59+00:00"
},
{
"name": "socialiteproviders/linkedin",
"version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/LinkedIn.git",
"reference": "bcf8a732328e868261416a5f04135acb0b94bf9a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/LinkedIn/zipball/bcf8a732328e868261416a5f04135acb0b94bf9a",
"reference": "bcf8a732328e868261416a5f04135acb0b94bf9a",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^7.2 || ^8.0",
"socialiteproviders/manager": "~4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"SocialiteProviders\\LinkedIn\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Faust",
"email": "hello@brianfaust.de"
}
],
"description": "LinkedIn OAuth2 Provider for Laravel Socialite",
"support": {
"source": "https://github.com/SocialiteProviders/LinkedIn/tree/4.1.0"
},
"time": "2020-12-01T23:10:59+00:00"
},
{
"name": "socialiteproviders/manager",
"version": "4.0.1",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Manager.git",
"reference": "0f5e82af0404df0080bdc5c105cef936c1711524"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/0f5e82af0404df0080bdc5c105cef936c1711524",
"reference": "0f5e82af0404df0080bdc5c105cef936c1711524",
"shasum": ""
},
"require": {
"illuminate/support": "^6.0|^7.0|^8.0",
"laravel/socialite": "~4.0|~5.0",
"php": "^7.2 || ^8.0"
},
"require-dev": {
"mockery/mockery": "^1.2",
"phpunit/phpunit": "^9.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"SocialiteProviders\\Manager\\ServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"SocialiteProviders\\Manager\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Andy Wendt",
"email": "andy@awendt.com"
},
{
"name": "Anton Komarev",
"email": "a.komarev@cybercog.su"
},
{
"name": "Miguel Piedrafita",
"email": "soy@miguelpiedrafita.com"
},
{
"name": "atymic",
"email": "atymicq@gmail.com",
"homepage": "https://atymic.dev"
}
],
"description": "Easily add new or override built-in providers in Laravel Socialite.",
"homepage": "https://socialiteproviders.com/",
"support": {
"issues": "https://github.com/SocialiteProviders/Manager/issues",
"source": "https://github.com/SocialiteProviders/Manager/tree/4.0.1"
},
"time": "2020-12-01T23:09:06+00:00"
},
{
"name": "sokil/php-isocodes",
"version": "3.3.4",

View File

@ -170,6 +170,8 @@ return [
LaravelDoctrine\Extensions\BeberleiExtensionsServiceProvider::class,
\App\Models\Utils\MySQLExtensionsServiceProvider::class,
\App\libs\Utils\FileSystem\SwiftServiceProvider::class,
// remove 'Laravel\Socialite\SocialiteServiceProvider',
\SocialiteProviders\Manager\ServiceProvider::class, // add
],
/*
@ -223,6 +225,7 @@ return [
'EntityManager' => LaravelDoctrine\ORM\Facades\EntityManager::class,
'Registry' => LaravelDoctrine\ORM\Facades\Registry::class,
'Doctrine' => LaravelDoctrine\ORM\Facades\Doctrine::class,
'Socialite' => Laravel\Socialite\Facades\Socialite::class,
],
'version' => env('APP_VERSION', 'XX.XX.XX'),

View File

@ -39,4 +39,25 @@ return [
'api_key' => env('SENDGRID_API_KEY'),
],
// 3rd party idps
'facebook' => [
'client_id' => env('FACEBOOK_CLIENT_ID'),
'client_secret' => env('FACEBOOK_CLIENT_SECRET'),
'redirect' => env('FACEBOOK_REDIRECT_URI')
],
'google' => [
'client_id' => env('GOOGLE_CLIENT_ID'),
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
'redirect' => env('GOOGLE_REDIRECT_URI')
],
'apple' => [
'client_id' => env('APPLE_CLIENT_ID'),
'client_secret' => env('APPLE_CLIENT_SECRET'),
'redirect' => env('APPLE_REDIRECT_URI')
],
'linkedin' => [
'client_id' => env('LINKEDIN_CLIENT_ID'),
'client_secret' => env('LINKEDIN_CLIENT_SECRET'),
'redirect' => env('LINKEDIN_REDIRECT_URI')
],
];

View File

@ -0,0 +1,53 @@
<?php namespace Database\Migrations;
/**
* Copyright 2021 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Doctrine\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema as Schema;
use LaravelDoctrine\Migrations\Schema\Builder;
use LaravelDoctrine\Migrations\Schema\Table;
/**
* Class Version20210512181715
* @package Database\Migrations
*/
class Version20210512181715 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(Schema $schema): void
{
$builder = new Builder($schema);
if($schema->hasTable("users") && !$builder->hasColumn("users","external_provider") ) {
$builder->table('users', function (Table $table) {
$table->string('external_provider')->setNotnull(false)->setLength(254);
$table->string('external_id')->setNotnull(false)->setLength(254);
$table->string('external_pic')->setNotnull(false)->setLength(254);
});
}
}
/**
* @param Schema $schema
*/
public function down(Schema $schema): void
{
$builder = new Builder($schema);
if($schema->hasTable("users") && $builder->hasColumn("users","external_provider") ) {
$builder->table('users', function (Table $table) {
$table->dropColumn('external_provider');
$table->dropColumn('external_id');
$table->dropColumn('external_pic');
});
}
}
}

14069
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,38 +5,77 @@
"main": "resources/assets/js/index.js",
"author": "smarcet@gmail.com",
"scripts": {
"build-dev": "./node_modules/.bin/webpack --config webpack.config.js",
"build": "NODE_ENV=production ./node_modules/.bin/webpack --config webpack.config.js"
"build-dev": "./node_modules/.bin/webpack --config webpack.dev.js",
"build": "./node_modules/.bin/webpack --config webpack.prod.js",
"serve": "webpack-dev-server --open --port=8888 --https --config webpack.dev.js",
"test": "jest --watch"
},
"devDependencies": {
"webpack": "^3.11.0",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.4",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-flow": "^6.23.0",
"css-loader": "^0.28.10",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.11",
"json-loader": "^0.5.7",
"less": "^2.7.3",
"less-loader": "^4.1.0",
"postcss-loader": "^2.1.3",
"@babel/cli": "^7.13.10",
"@babel/core": "^7.13.10",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
"@babel/preset-env": "^7.13.12",
"@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.13.13",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.6",
"babel-jest": "^26.6.3",
"babel-loader": "^8.0.6",
"babel-plugin-react-remove-properties": "^0.3.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^6.4.1",
"css-loader": "^3.1.0",
"dotenv-webpack": "^1.7.0",
"file-loader": "^4.1.0",
"font-awesome": "4.7.0",
"history": "^4.7.2",
"html-webpack-plugin": "^3.2.0",
"i18n-react": "^0.6.4",
"identity-obj-proxy": "^3.0.0",
"immutability-helper": "^2.7.1",
"jest": "^26.6.3",
"mini-css-extract-plugin": "^0.8.0",
"node-sass": "^4.12.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"path": "^0.12.7",
"react": "^16.6.3",
"react-bootstrap": "^0.33.1",
"react-datetime": "^2.15.0",
"react-dom": "^16.4.1",
"react-redux": "^5.0.7",
"react-rte-ref-fix": "^0.16.2",
"react-test-renderer": "^17.0.2",
"react-transition-group": "^1.2.1",
"redux": "^3.7.2",
"redux-mock-store": "^1.5.4",
"redux-persist": "^5.10.0",
"redux-thunk": "^2.3.0",
"regenerator-runtime": "^0.13.7",
"sass-loader": "^6.0.7",
"style-loader": "^0.19.1",
"url-loader": "^0.6.2"
"superagent": "^6.1.0",
"timesync": "^1.0.5",
"urijs": "^1.19.1",
"url-loader": "^0.6.2",
"webpack": "^4.29.0",
"webpack-cli": "^4.7.0",
"webpack-dev-server": "^3.1.14",
"webpack-merge": "^4.2.1",
"webpack-node-externals": "^1.7.2"
},
"dependencies": {
"@github/clipboard-copy-element": "^1.1.2",
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"bootstrap": "^3.3.7",
"bootstrap-datepicker": "^1.8.0",
"bootstrap-sass": "^3.0.0",
"bootstrap-tagsinput": "^0.7.1",
"chosen-js": "^1.8.7",
"copy-webpack-plugin": "^4.5.1",
"crypto-js": "^3.1.9-1",
"file-loader": "^1.1.11",
"font-awesome": "^4.7.0",
"jquery": "~2.1.4",
"jquery-migrate": "1.2.1",
@ -44,11 +83,13 @@
"jquery-validation": "^1.17.0",
"jquery.cookie": "^1.4.1",
"jqueryui": "^1.11.1",
"laravel-elixir": "^5.0.0",
"jsdom": "^16.5.3",
"moment": "^2.24.0",
"moment-timezone": "^0.5.21",
"popper.js": "^1.14.3",
"pure": "^2.85.0",
"pwstrength-bootstrap": "^2.2.1",
"pwstrength-bootstrap": "^3.0.10",
"react-google-recaptcha": "^2.1.0",
"simplemde": "^1.11.2",
"sweetalert2": "7.3.5",
"typeahead.js": "^0.11.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

View File

@ -0,0 +1,70 @@
import request from 'superagent';
import URI from "urijs";
let http = request;
import Swal from 'sweetalert2';
export const createAction = type => payload => ({
type,
payload
});
export const RESET_LOADING = 'RESET_LOADING';
export const START_LOADING = 'START_LOADING';
export const STOP_LOADING = 'STOP_LOADING';
export const resetLoading = createAction(RESET_LOADING);
export const startLoading = createAction(START_LOADING);
export const stopLoading = createAction(STOP_LOADING);
const xhrs = {};
const cancel = (key) => {
if(xhrs[key]) {
xhrs[key].abort();
console.log(`aborted request ${key}`);
delete xhrs[key];
}
}
const schedule = (key, req) => {
// console.log(`scheduling ${key}`);
xhrs[key] = req;
};
const isObjectEmpty = (obj) => {
return Object.keys(obj).length === 0 && obj.constructor === Object ;
}
export const getRawRequest = (endpoint, errorHandler = null) => (params) => {
let url = URI(endpoint);
if(!isObjectEmpty(params))
url = url.query(params);
let key = url.toString();
cancel(key);
return new Promise((resolve, reject) => {
let req = http.get(url.toString())
.timeout({
response: 60000,
deadline: 60000,
})
.end(
(err, res) => {
if (err || !res.ok) {
if(errorHandler) {
errorHandler(err, res);
}
return reject({ err, res })
}
let json = res.body;
return resolve({response: json});
}
)
schedule(key, req);
});
}

20
resources/js/bootstrap.js vendored Normal file
View File

@ -0,0 +1,20 @@
import 'font-awesome/css/font-awesome.min.css';
import 'jquery-ui-themes/themes/ui-darkness/jquery-ui.css';
import 'sweetalert2/dist/sweetalert2.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-datepicker/dist/css/bootstrap-datepicker.css'
window._ = require('lodash');
import $ from 'jquery';
window.jQuery = $;
window.$ = $;
import 'bootstrap'
import 'jqueryui'
import 'jquery-validation';
import 'jquery-validation/dist/additional-methods.js';
import swal from 'sweetalert2';
import 'pure';
import 'pwstrength-bootstrap/dist/pwstrength-bootstrap.js';
import 'bootstrap-datepicker/dist/js/bootstrap-datepicker.js';

View File

@ -0,0 +1,34 @@
import React from "react";
import { makeStyles } from "@material-ui/core";
const useStyles = makeStyles(theme => ({
container: {
display: "flex",
alignItems: "center"
},
border: {
borderBottom: "1px solid #3fa2f7",
width: "100%"
},
content: {
paddingTop: theme.spacing(0.5),
paddingBottom: theme.spacing(0.5),
paddingRight: theme.spacing(2),
paddingLeft: theme.spacing(2),
fontWeight: 500,
fontSize: 16,
color: "#3fa2f7"
}
}));
const DividerWithText = ({ children }) => {
const classes = useStyles();
return (
<div className={classes.container}>
<div className={classes.border} />
<span className={classes.content}>{children}</span>
<div className={classes.border} />
</div>
);
};
export default DividerWithText;

View File

@ -10,20 +10,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
import 'font-awesome/css/font-awesome.min.css';
import 'jquery-ui-themes/themes/ui-darkness/jquery-ui.css';
import 'sweetalert2/dist/sweetalert2.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-datepicker/dist/css/bootstrap-datepicker.css'
import $ from 'jquery';
window.jQuery = $;
window.$ = $;
import 'bootstrap'
import 'jqueryui'
import 'jquery-validation';
import 'jquery-validation/dist/additional-methods.js';
import swal from 'sweetalert2';
import 'pure';
import 'pwstrength-bootstrap/dist/pwstrength-bootstrap.js';
import 'bootstrap-datepicker/dist/js/bootstrap-datepicker.js';
import './bootstrap';

View File

@ -0,0 +1,12 @@
import {getRawRequest} from '../base_actions'
export const verifyAccount = (email) => {
const params = {
email: email
};
return getRawRequest(window.VERIFY_ACCOUNT_ENDPOINT)(params);
}

427
resources/js/login/login.js Normal file
View File

@ -0,0 +1,427 @@
import styles from './login.module.scss'
import "./third_party_identity_providers.scss";
import React from 'react';
import ReCAPTCHA from "react-google-recaptcha";
import ReactDOM from 'react-dom';
import Avatar from '@material-ui/core/Avatar';
import Button from '@material-ui/core/Button';
import CssBaseline from '@material-ui/core/CssBaseline';
import TextField from '@material-ui/core/TextField';
import Link from '@material-ui/core/Link';
import Typography from '@material-ui/core/Typography';
import Paper from '@material-ui/core/Paper';
import Container from '@material-ui/core/Container';
import Chip from '@material-ui/core/Chip';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';
import {verifyAccount} from './actions';
import {MuiThemeProvider, createMuiTheme} from '@material-ui/core/styles';
import DividerWithText from '../components/divider_with_text';
import Visibility from '@material-ui/icons/Visibility';
import VisibilityOff from '@material-ui/icons/VisibilityOff';
import InputAdornment from '@material-ui/core/InputAdornment';
import IconButton from '@material-ui/core/IconButton';
import {emailValidator} from '../validator';
import Grid from '@material-ui/core/Grid';
const EmailInputForm = ({onValidateEmail, onHandleUserNameChange, disableInput, emailError}) => {
return (
<Paper elevation={0} component="form"
target="_self"
className={styles.paper_root}
onSubmit={onValidateEmail}>
<TextField
id="email"
name="email"
autoComplete="email"
variant="outlined"
margin="normal"
required
fullWidth
disabled={disableInput}
label="Email Address"
autoFocus
onChange={onHandleUserNameChange}
error={emailError != ""}
helperText={emailError}
/>
{emailError == "" &&
<Button variant="contained"
color="primary"
title="Continue"
className={styles.apply_button}
disabled={disableInput}
onClick={onValidateEmail}>
&gt;
</Button>
}
</Paper>
);
}
const PasswordInputForm = ({
formAction,
onAuthenticate,
disableInput,
showPassword,
passwordValue,
passwordError,
onUserPasswordChange,
handleClickShowPassword,
handleMouseDownPassword,
userNameValue,
csrfToken,
shouldShowCaptcha,
captchaPublicKey,
onChangeRecaptcha
}) => {
return(
<form method="post" action={formAction} onSubmit={onAuthenticate} target="_self">
<TextField
id="password"
name="password"
disabled={disableInput}
type={showPassword ? 'text' : 'password'}
value={passwordValue}
variant="outlined"
margin="normal"
required
fullWidth
label="Enter Your Password"
autoComplete="current-password"
error={passwordError != ""}
helperText={passwordError}
onChange={onUserPasswordChange}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
>
{showPassword ? <Visibility/> : <VisibilityOff/>}
</IconButton>
</InputAdornment>
)
}}
/>
<FormControlLabel
disabled={disableInput}
control={<Checkbox value="remember" name="remember" id="remember" color="primary"/>}
label="Remember me"
/>
<input type="hidden" value={userNameValue} id="username" name="username"/>
<input type="hidden" value={csrfToken} id="_token" name="_token"/>
{shouldShowCaptcha() &&
<ReCAPTCHA
className={styles.recaptcha}
sitekey={captchaPublicKey}
onChange={onChangeRecaptcha}
/>
}
<Button variant="contained"
disabled={disableInput}
className={styles.continue_btn}
color="primary"
type="submit"
onClick={onAuthenticate}>
Continue
</Button>
</form>
);
}
const HelpLinks = ({forgotPasswordAction, verifyEmailAction, helpAction, appName}) => {
return (
<>
<hr className={styles.separator}/>
<Link href={forgotPasswordAction} target="_self" variant="body2">
Forgot password?
</Link>
<Link href={verifyEmailAction} target="_self" variant="body2">
Verify {appName}
</Link>
<Link href={helpAction} variant="body2" target="_self">
Having trouble?
</Link>
</>
);
}
const EmailErrorActions = ({createAccountAction, onValidateEmail, disableInput}) => {
return(
<Grid container style={{alignItems: 'center', marginTop: "20%"}}>
<Grid item xs>
<Link href={createAccountAction} variant="body2" target="_self">
Create Account
</Link>
</Grid>
<Grid item>
<Button variant="contained"
onClick={onValidateEmail}
disabled={disableInput}
color="primary">
Continue
</Button>
</Grid>
</Grid>
);
}
const ThirdPartyIdentityProviders = ({thirdPartyProviders, formAction, disableInput}) => {
return(
<>
<DividerWithText>Or</DividerWithText>
{
thirdPartyProviders.map((provider) => {
return (
<Button
disabled={disableInput}
key={provider.name}
variant="contained"
className={styles.third_party_idp_button+` ${provider.name}`}
color="primary"
title={`Sign In with ${provider.label}`}
href={`${formAction}/${provider.name}`}>
{provider.label}
</Button>
);
})
}
</>
);
}
class LoginPage extends React.Component {
constructor(props) {
super(props);
this.state = {
user_name: props.userName,
user_password: '',
user_pic: props.hasOwnProperty('user_pic') ? props.user_pic : null,
user_fullname: props.hasOwnProperty('user_fullname') ? props.user_fullname : null,
user_verified: props.hasOwnProperty('user_verified') ? props.user_verified : false,
errors: {
email: "",
password: props.authError != "" ? props.authError : "",
},
captcha_value: '',
showPassword: false,
disableInput: false,
}
this.onHandleUserNameChange = this.onHandleUserNameChange.bind(this);
this.onValidateEmail = this.onValidateEmail.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.onAuthenticate = this.onAuthenticate.bind(this);
this.onChangeRecaptcha = this.onChangeRecaptcha.bind(this);
this.onUserPasswordChange = this.onUserPasswordChange.bind(this);
this.shouldShowCaptcha = this.shouldShowCaptcha.bind(this);
this.handleClickShowPassword = this.handleClickShowPassword.bind(this);
this.handleMouseDownPassword = this.handleMouseDownPassword.bind(this);
}
shouldShowCaptcha() {
return (
this.props.hasOwnProperty('maxLoginAttempts2ShowCaptcha') &&
this.props.hasOwnProperty('loginAttempts') &&
this.props.loginAttempts >= this.props.maxLoginAttempts2ShowCaptcha
)
}
onAuthenticate(ev) {
if (this.state.user_password == '') {
this.setState({...this.state, errors: {...this.state.errors, password: 'Password is empty'}});
ev.preventDefault();
return false;
}
if (this.state.captcha_value == '' && this.shouldShowCaptcha()) {
this.setState({...this.state, errors: {...this.state.errors, password: 'you must check CAPTCHA'}});
ev.preventDefault();
return false;
}
return true;
}
onChangeRecaptcha(value) {
this.setState({...this.state, captcha_value: value});
}
onHandleUserNameChange(ev) {
let {value, id} = ev.target;
this.setState({...this.state, user_name: value});
}
onUserPasswordChange(ev) {
let {errors} = this.state;
let {value, id} = ev.target;
if(value == "") // clean error
errors[id] = '';
this.setState({...this.state, user_password: value, errors: {...errors}});
}
onValidateEmail(ev) {
ev.preventDefault();
if (this.state.user_name == '') {
return false;
}
if (!emailValidator(this.state.user_name)) {
return false;
}
this.setState({...this.state, disableInput: true});
verifyAccount(this.state.user_name).then((payload) => {
let {response} = payload;
this.setState({
...this.state,
user_pic: response.pic,
user_fullname: response.full_name,
user_verified: true,
errors: {
email: '',
password: ''
},
disableInput: false
})
}, (error) => {
let {body} = error.res;
let newErrors = {}
newErrors['password'] = '';
newErrors['email'] = "We could not find an Account with that email Address";
this.setState({
...this.state,
user_pic: null,
user_fullname: null,
user_verified: false,
errors: newErrors,
disableInput: false
});
});
return true;
}
handleDelete() {
this.setState({...this.state, user_name: null, user_pic: null, user_fullname: null, user_verified: false});
}
handleClickShowPassword(ev) {
this.setState({...this.state, showPassword: !this.state.showPassword})
}
handleMouseDownPassword(ev) {
ev.preventDefault();
}
render() {
return (
<Container component="main" maxWidth="xs" className={styles.main_container}>
<CssBaseline/>
<div className={styles.inner_container}>
<Typography component="h1">
<a href={window.location.href}><img className={styles.app_logo} alt="appLogo" src={this.props.appLogo}/></a>
</Typography>
<Typography component="h1" variant="h5">
Sign in {this.state.user_fullname && <Chip
avatar={<Avatar alt={this.state.user_fullname} src={this.state.user_pic}/>}
variant="outlined"
className={styles.valid_user_name_chip}
label={this.state.user_fullname}
onDelete={this.handleDelete}/>}
</Typography>
{!this.state.user_verified &&
<>
<EmailInputForm
onValidateEmail={this.onValidateEmail}
onHandleUserNameChange={this.onHandleUserNameChange}
disableInput={this.state.disableInput}
emailError={this.state.errors.email}/>
{ this.state.errors.email == '' &&
this.props.thirdPartyProviders.length > 0 &&
<ThirdPartyIdentityProviders
thirdPartyProviders={this.props.thirdPartyProviders}
formAction={this.props.formAction}
disableInput={this.state.disableInput}
/>
}
{
// we already had an interaction and got an user error...
this.state.errors.email != '' &&
<>
<EmailErrorActions
onValidateEmail={this.onValidateEmail}
disableInput={this.state.disableInput}
createAccountAction={(this.state.user_name) ? `${this.props.createAccountAction}?email=${encodeURIComponent(this.state.user_name)}`: this.props.createAccountAction}
/>
<HelpLinks
appName={this.props.appName}
forgotPasswordAction={this.props.forgotPasswordAction}
verifyEmailAction={this.props.verifyEmailAction}
helpAction={this.props.helpAction}
/>
</>
}
</>
}
{this.state.user_verified &&
// proceed to ask for password ( 2nd step )
<>
<PasswordInputForm
formAction={this.props.formAction}
onAuthenticate={this.onAuthenticate}
disableInput={this.state.disableInput}
showPassword={this.state.showPassword}
passwordValue={this.state.user_password}
passwordError={this.state.errors.password}
onUserPasswordChange={this.onUserPasswordChange}
handleClickShowPassword={this.handleClickShowPassword}
handleMouseDownPassword={this.handleMouseDownPassword}
userNameValue={this.state.user_name}
csrfToken={this.props.token}
shouldShowCaptcha={this.shouldShowCaptcha}
captchaPublicKey={this.props.captchaPublicKey}
onChangeRecaptcha={this.onChangeRecaptcha}
/>
<HelpLinks
appName={this.props.appName}
forgotPasswordAction={this.props.forgotPasswordAction}
verifyEmailAction={this.props.verifyEmailAction}
helpAction={this.props.helpAction}
/>
</>
}
</div>
</Container>
);
}
}
// Or Create your Own theme:
const theme = createMuiTheme({
palette: {
primary: {
main: '#3fa2f7'
},
},
overrides: {
MuiButton: {
containedPrimary: {
color: 'white'
}
}
}
});
ReactDOM.render(
<MuiThemeProvider theme={theme}>
<LoginPage {...config}/>
</MuiThemeProvider>,
document.querySelector('#root')
);

View File

@ -0,0 +1,74 @@
$border-color: #e3e3e3;
$base-color:#3fa2f7;
.main_container{
border: solid 1px $border-color;
margin-top: 10%;
margin-bottom: 10%;
color: $base-color;
}
.inner_container {
display: flex;
margin-top: 64px;
padding-bottom: 20%;
flex-direction: column;
.app_logo{
max-width: 40%;
float: right;
margin-bottom: 15%;
}
.continue_btn{
float: right;
margin-top: 30%;
}
.recaptcha{
width: 100%;
margin-top: 10px;
margin-bottom: 10px;
}
.separator{
color:$base-color;
width: 100%;
border: 1px solid;
margin-bottom: 5%;
margin-top: 5%;
}
}
.paper_root {
padding: 2px 4px;
display: flex;
align-items: center;
.apply_button {
height: 55px;
margin-bottom: 8px;
margin-top: 16px;
margin-left: 10px;
min-width: 20px !important;
font-size: 20pt;
}
}
.third_party_idp_button {
width: 100%;
margin-top: 5px !important;
margin-bottom: 5px !important;
position:relative;
box-sizing: border-box;
}
.third_party_idp_button:before{
content: "";
box-sizing: border-box;
position: absolute;
top: 0;
left: 0;
width: 34px;
height: 100%;
}
.valid_user_name_chip{
float: right;
}

View File

@ -0,0 +1,60 @@
$facebook_base_color:#3B5998;
$apple_base_color:#000000;
$google_base_color:#DD4B39;
$linkedin_base_color:#2867B2;
.facebook{
background-color: $facebook_base_color !important;
}
.facebook:hover,
.facebook:focus {
background-color: #5B7BD5;
background-image: linear-gradient(#5B7BD5, #4864B1);
}
.facebook:before {
border-right: #364e92 1px solid !important;
background: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/14082/icon_facebook.png') 6px 6px no-repeat !important;
}
.apple{
background-color: $apple_base_color !important;
}
.apple:before {
border-right: #000001 1px solid !important;
background: url('/assets/img/third_party_identity_providers/icon_apple.png') 6px 6px no-repeat !important;
}
.google:before {
border-right: #BB3F30 1px solid;
background: url('/assets/img/third_party_identity_providers/icon_google.png') 6px 6px no-repeat;
}
.google {
background-color: $google_base_color !important;
}
.google:hover,
.google:focus {
background: #E74B37;
}
.google:before {
border-right: #BB3F30 1px solid;
background: url('/assets/img/third_party_identity_providers/icon_google.png') 6px 6px no-repeat;
}
.linkedin{
background-color: $linkedin_base_color !important;
}
.linkedin:hover,
.linkedin:focus {
background-color: #2867B3;
background-image: linear-gradient(#5B7BD5, #4864B1);
}
.linkedin:before {
border-right: #4864B1 1px solid;
background: url('/assets/img/third_party_identity_providers/icon_linkedin.png') 6px 6px no-repeat;
}

View File

@ -0,0 +1,3 @@
export const emailValidator = (value) => {
return /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test( value )
}

View File

@ -34,7 +34,9 @@
</div>
@endif
<div class="signup-form">
<form id="form-verification" method="POST" action="{{ URL::action('Auth\EmailVerificationController@resend') }}">
<form id="form-verification"
target="_self"
method="POST" action="{{ URL::action('Auth\EmailVerificationController@resend') }}">
@csrf
<h2>{{ __('Email Verification') }}</h2>
<div class="form-group">

View File

@ -1,114 +1,71 @@
@extends('layout')
@extends('auth_layout')
@section('title')
<title>Welcome to {{ Config::get("app.app_name") }} - Sign in </title>
@append
@section('meta')
<meta http-equiv="X-XRDS-Location" content="{!! URL::action("OpenId\DiscoveryController@idp") !!}"/>
<meta name="csrf-token" content="{{ csrf_token() }}">
@append
@section('css')
{!! HTML::style('assets/css/auth/login.css') !!}
{!! HTML::style('assets/css/login.css') !!}
@append
@section('content')
@if(isset($identity_select))
<legend style="margin-left: 15px;">
@if(!$identity_select)
Sign in to <b>{!! $realm !!}</b> using <b>{!! $identity !!}</b>
@else
Sign in to <b>{!! $realm !!} </b> using your {{ Config::get("app.app_name") }}
@endif
</legend>
@endif
<div id="cookies-disabled-dialog" class="alert alert-warning alert-dismissible" style="display: none;" role="alert">
<button type="button" class="close" onclick="$('#cookies-disabled-dialog').hide()" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<strong>Warning!</strong> Cookies are not enabled, please enabled them in order to use {{ Config::get("app.app_name") }}.
</div>
<div class="col-md-4" id="sidebar">
<div class="well">
{!! Form::open(array('id'=>'login_form','url' => URL::action('UserController@postLogin'), 'method' => 'post', "autocomplete" => "off")) !!}
<legend>
Welcome&nbsp;to&nbsp;{{ Config::get("app.app_name") }}!&nbsp;<span aria-hidden="true" style="font-size: 10pt;"
class="glyphicon glyphicon-info-sign pointable"
title="Please use your {{ Config::get("app.app_name") }} to log in"></span>
</legend>
@if(Config::get("app.app_info"))
<p class="help-block">
{{Config::get("app.app_info")}}
</p>
@endif
<div class="form-group">
{!! Form::email('username',Session::has('username')? Session::get('username'):null, array
(
'placeholder' => 'Username',
'class' =>'form-control',
'required' => 'true',
'autocomplete' => 'username'
)) !!}
</div>
<div class="form-group">
<input placeholder="Password" class="form-control" required="true" autocomplete="current-password" name="password" id="password" type="password" value="">
<span toggle="#password" class="fa fa-fw fa-eye fa-eye-slash field-icon toggle-password" title="Show Password"/>
</div>
<div class="form-group">
@if(Session::has('flash_notice'))
<span class="error-message"><i
class="fa fa-exclamation-triangle">&nbsp;{!! Session::get('flash_notice') !!}</i></span>
@else
@foreach($errors->all() as $message)
<span class="error-message"><i
class="fa fa-exclamation-triangle">&nbsp;{!! $message !!}</i></span>
@endforeach
@endif
</div>
@if(Session::has('login_attempts') && Session::has('max_login_attempts_2_show_captcha') && Session::get('login_attempts') > Session::get('max_login_attempts_2_show_captcha'))
{!! Recaptcha::render(array('id'=>'captcha','class'=>'input-block-level')) !!}
{!! Form::hidden('login_attempts', Session::get('login_attempts')) !!}
@else
{!! Form::hidden('login_attempts', '0') !!}
@endif
<div class="checkbox">
<label class="checkbox">
{!! Form::checkbox('remember', '1', false) !!}Remember me
</label>
</div>
<div class="pull-right">
{!! Form::submit('Sign In',array('id'=>'login','class'=>'btn btn-primary')) !!}
<a class="btn btn-primary" href="{!! URL::action('UserController@cancelLogin') !!} ">Cancel</a>
</div>
<div style="clear:both;padding-top:15px;" class="row">
<div class="col-md-12">
<a title="register new account" target="_blank"
href="{!! URL::action("Auth\RegisterController@showRegistrationForm") !!}">Create an
{!! Config::get("app.app_name") !!} </a>
</div>
</div>
<div style="clear:both;padding-top:15px;" class="row">
<div class="col-md-12">
<a title="forgot password" target="_blank"
href="{!! URL::action("Auth\ForgotPasswordController@showLinkRequestForm") !!}">Forgot password?</a>
</div>
</div>
<div style="clear:both;padding-top:15px;" class="row">
<div class="col-md-12">
<a title="verify account" target="_blank" href="{!! URL::action("Auth\EmailVerificationController@showVerificationForm") !!}">Verify
{!! Config::get("app.app_name") !!}</a>
</div>
</div>
<div style="clear:both;padding-top:15px;" class="row">
<div class="col-md-12">
<a title="help" target="_blank" href="mailto:{!! Config::get("app.help_email") !!}">Help</a>
</div>
</div>
</fieldset>
{!! Form::close() !!}
</div>
</div>
<div class="col-md-8">
</div>
@append
@section('scripts')
{!! HTML::script('assets/js/login.js') !!}
<script>
let authError = '';
@if(Session::has('flash_notice'))
authError = '{!! Session::get("flash_notice") !!}';
@else
@foreach($errors->all() as $message)
authError = '{!! $message !!}';
@endforeach
@endif
let config = {
token : document.head.querySelector('meta[name="csrf-token"]').content,
userName:'{{ Session::has('username') ? Session::get('username') : ""}}',
realm: '{{isset($identity_select) ? $realm : ""}}',
appName: '{{ Config::get("app.app_name") }}',
appLogo: '{{ Config::get("app.logo_url") }}',
formAction: '{{ URL::action("UserController@postLogin") }}',
accountVerifyAction : '{{URL::action("UserController@getAccount")}}',
authError: authError,
captchaPublicKey: '{{ Config::get("recaptcha.public_key") }}',
thirdPartyProviders: [
@foreach($supported_providers as $provider => $label)
{label: "{{$label}}", name:"{{$provider}}"},
@endforeach
],
forgotPasswordAction:'{{ URL::action("Auth\ForgotPasswordController@showLinkRequestForm") }}',
verifyEmailAction:'{{ URL::action("Auth\EmailVerificationController@showVerificationForm") }}',
helpAction:'mailto:{!! Config::get("app.help_email") !!}',
createAccountAction:'{{ URL::action("Auth\RegisterController@showRegistrationForm") }}',
}
@if(Session::has('max_login_attempts_2_show_captcha'))
config.maxLoginAttempts2ShowCaptcha = {{Session::get("max_login_attempts_2_show_captcha")}};
@endif
@if(Session::has('login_attempts'))
config.loginAttempts = {{Session::get("login_attempts")}};
@endif
@if(Session::has('user_fullname'))
config.user_fullname = '{{Session::get("user_fullname")}}';
@endif
@if(Session::has('user_pic'))
config.user_pic = '{{Session::get("user_pic")}}';
@endif
@if(Session::has('user_verified'))
config.user_verified = {{Session::get('user_verified')}};
@endif
window.VERIFY_ACCOUNT_ENDPOINT = config.accountVerifyAction;
</script>
{!! HTML::script('assets/login.js') !!}
@append

View File

@ -35,7 +35,9 @@
</div>
@endif
<div class="signup-form">
<form id="form-send-password-reset-link" method="POST" action="{{ URL::action('Auth\ForgotPasswordController@sendResetLinkEmail') }}">
<form id="form-send-password-reset-link"
target="_self"
method="POST" action="{{ URL::action('Auth\ForgotPasswordController@sendResetLinkEmail') }}">
@csrf
<h2>Forgot Password?</h2>
<p class="hint-text">You can reset your password here.</p>

View File

@ -36,6 +36,7 @@
@endif
<div class="signup-form">
<form id="form-password-reset" class="form-horizontal" method="POST"
target="_self"
action="{{ URL::action('Auth\ResetPasswordController@reset') }}">
@csrf
<input type="hidden" name="token" value="{{ $token }}">

View File

@ -36,6 +36,7 @@
@endif
<div class="signup-form">
<form id="form-password-set" class="form-horizontal" method="POST"
target="_self"
action="{{ URL::action('Auth\PasswordSetController@setPassword') }}">
@csrf
<input type="hidden" name="token" value="{{ $token }}">

View File

@ -29,7 +29,9 @@
</div>
@endif
<div class="col-xs-12 col-md-5 col-md-offset-3 signup-form">
<form id="form-registration" method="POST" autocomplete="off" action="{{ URL::action('Auth\RegisterController@register') }}">
<form id="form-registration"
target="_self"
method="POST" autocomplete="off" action="{{ URL::action('Auth\RegisterController@register') }}">
@csrf
<h2>Register</h2>
<p class="hint-text">Create your account. It's free and only takes a minute.</p>

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
@yield('title')
<base href="{!! Config::get('app.url') !!}" target="_self">
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width"
/>
<link rel="shortcut icon" href="{!! Config::get('app.tenant_favicon') !!}" />
@yield('meta')
@yield('css')
<!--https://material-ui.com/getting-started/installation/-->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<style type="text/css">
#logo a {
background: url("{!! Config::get('app.logo_url') !!}") no-repeat scroll left center rgba(0, 0, 0, 0);
}
</style>
</head>
<body>
<div id="root">
@yield('content')
</div>
@yield('scripts')
<span style="display: none">{!! Config::get('app.version') !!}</span>
</body>
</html>

View File

@ -16,12 +16,12 @@
<div class="col-md-12">
<div class="row" style="padding-top: 5px;padding-bottom: 5px;">
<div class="col-md-12">
<a href="{!! URL::action("UserController@getLogin") !!}" class="btn btn-default btn-md active">Sign in to your account</a>
<a target="_self" href="{!! URL::action("UserController@getLogin") !!}" class="btn btn-default btn-md active">Sign in to your account</a>
</div>
</div>
<div class="row" style="padding-top: 5px;padding-bottom: 5px;">
<div class="col-md-12">
<a href="{!! URL::action("Auth\RegisterController@showRegistrationForm") !!}" class="btn btn-default btn-md active">Register for an {{ Config::get('app.app_name') }}</a>
<a target="_self" href="{!! URL::action("Auth\RegisterController@showRegistrationForm") !!}" class="btn btn-default btn-md active">Register for an {{ Config::get('app.app_name') }}</a>
</div>
</div>
</div>

View File

@ -2,6 +2,7 @@
<html lang="en">
<head>
@yield('title')
<base href="{!! Config::get('app.url') !!}" target="_blank">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="{!! Config::get('app.tenant_favicon') !!}" />
@yield('meta')
@ -33,7 +34,6 @@
</div>
<footer class="row"></footer>
</div>
{!! HTML::script('assets/__common__.js')!!}
{!! HTML::script('assets/index.js')!!}
{!! HTML::script('assets/js/ajax.utils.js')!!}
{!! HTML::script('assets/js/jquery.cleanform.js')!!}

View File

@ -28,7 +28,8 @@
<li> {!!$scope->getShortDescription()!!}&nbsp;<span class="glyphicon glyphicon-info-sign pointable info" aria-hidden="true" data-content="{!! $scope->getDescription() !!}" title="Scope Info"></span></li>
@endforeach
</ul>
{!! Form::open(array('url' => URL::action("UserController@postConsent") ,'id'=>'authorization_form', 'method' => 'post', "autocomplete" => "off")) !!}
{!! Form::open(array('url' => URL::action("UserController@postConsent") ,'id'=>'authorization_form', 'method' => 'post', "autocomplete" => "off", "target"=> "_self" )) !!}
<input type="hidden" name='trust' id='trust' value=""/>
<button class="btn btn-default btn-md btn-consent-action" id="cancel-authorization" type="button">Cancel</button>
<button class="btn btn-primary btn-md btn-consent-action" id="approve-authorization" type="button">Accept</button>

View File

@ -12,7 +12,7 @@ Welcome, <a href="{!! URL::action("UserController@getProfile") !!}">{!!Auth::use
@section('content')
<div class="container">
<h4>{!! Config::get('app.app_name') !!} - Openid verification</h4>
{!! Form::open(array('url' => URL::action("UserController@postConsent"),'id'=>'authorization_form', 'method' => 'post', "autocomplete" => "off")) !!}
{!! Form::open(array('url' => URL::action("UserController@postConsent"),'id'=>'authorization_form', 'method' => 'post', "autocomplete" => "off", "target" => "_self")) !!}
<legend>
Sign in to <b>{!! $realm !!}</b> using your {{ Config::get('app.app_name') }}
</legend>

View File

@ -41,10 +41,16 @@ Route::group(array('middleware' => ['ssl']), function () {
//user interaction
Route::group(array('prefix' => 'auth'), function () {
Route::group(array('prefix' => 'login'), function () {
Route::get('', "UserController@getLogin");
Route::get('account-verify', "UserController@getAccount");
Route::post('', ['middleware' => 'csrf', 'uses' => 'UserController@postLogin']);
Route::get('cancel', "UserController@cancelLogin");
Route::group(array('prefix' => '{provider}'), function () {
Route::get('', 'SocialLoginController@redirect')->name("social_login");
Route::any('callback','SocialLoginController@callback')->name("social_login_callback");
});
});
// registration routes

View File

@ -156,7 +156,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
$url = $response->getTargetUrl();
$response = $this->call('GET', $url);
$response = $this->call('GET', $url, [], [$response->cookie("openstackid_session")]);
$this->assertResponseStatus(200);

139
webpack.common.js Normal file
View File

@ -0,0 +1,139 @@
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const webpack = require('webpack');
const path = require('path');
module.exports = {
/*
optimization: {
splitChunks: {
cacheGroups: {
commons: {
name: 'commons',
chunks: 'initial',
minChunks: 2,
},
},
},
},
*/
entry: {
login: './resources/js/login/login.js',
index: './resources/js/index.js',
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'public/assets'),
publicPath: '/assets/',
pathinfo: false
},
node: {
fs: 'empty',
child_process: 'empty',
tls: 'empty',
net: 'empty',
},
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
}),
new MiniCssExtractPlugin({
filename: './css/[name].css',
}),
new CopyPlugin({
patterns: [
{from: './node_modules/bootstrap-tagsinput/dist', to: 'bootstrap-tagsinput'},
{from: './node_modules/typeahead.js/dist', to: 'typeahead'},
{from: './node_modules/jquery.cookie/jquery.cookie.js', to: 'jquery-cookie/jquery.cookie.js'},
{from: './node_modules/crypto-js/crypto-js.js', to: 'crypto-js/crypto-js.js'},
{from: './node_modules/pwstrength-bootstrap/dist', to: 'pwstrength-bootstrap'},
{from: './node_modules/sweetalert2/dist', to: 'sweetalert2'},
{from: './node_modules/urijs/src', to: 'urijs'},
{from: './node_modules/chosen-js', to: 'chosen-js'},
{from: './node_modules/moment', to: 'moment'},
{from: './node_modules/@github/clipboard-copy-element/dist', to: 'clipboard-copy-element'},
{from: './node_modules/simplemde/dist', to: 'simplemde'},
],
}
),
],
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
"@babel/preset-env",
{"targets": {"node": "current"}}
],
'@babel/preset-react',
'@babel/preset-flow'
],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-class-properties'
]
}
}
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
},
{
test: /\.module\.scss/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
localIdentName: "[local]___[hash:base64:5]",
hashPrefix: 'schedule-filter-widget',
},
sourceMap: false
}
},
{
loader: 'sass-loader',
options: {
sourceMap: false
}
}
]
},
{
test: /\.scss/,
exclude: /\.module\.scss/,
use: [
MiniCssExtractPlugin.loader,
// Translates CSS into CommonJS
"css-loader",
// Compiles Sass to CSS
"sass-loader",
],
},
{
test: /\.(ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: "file-loader?name=fonts/[name].[ext]"
},
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: "url-loader?limit=10000&minetype=application/font-woff&name=fonts/[name].[ext]"
},
{
test: /\.svg/,
use: "file-loader?name=svg/[name].[ext]!svgo-loader"
},
{
test: /\.jpg|\.png|\.gif$/,
use: "file-loader?name=images/[name].[ext]"
},
]
}
};

18
webpack.dev.js Normal file
View File

@ -0,0 +1,18 @@
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
watch:true,
plugins: [
//new CleanWebpackPlugin(),
],
mode: 'development',
devtool: 'inline-source-map',
devServer: {
contentBase: './public/assets',
historyApiFallback: true
},
optimization: {
minimize: false
},
});

12
webpack.prod.js Normal file
View File

@ -0,0 +1,12 @@
const path = require('path');
//const TerserJSPlugin = require('terser-webpack-plugin');
//const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
optimization: {
minimize: true
},
});

10585
yarn.lock Normal file

File diff suppressed because it is too large Load Diff