I am trying to configure guard with an OAuth 2 connection.
I am trying to do this with a redirection in the getCredentials function to the Microsoft login website but I can't make it work. I don't know how I can make it worked.
It seems there is no redirection possible in this function.
public function getCredentials(Request $request)
{
$provider = new Microsoft([
'clientId' => '0000000032624',
'clientSecret' => 'my-secret',
'redirectUri' => 'https://mysite/oauthlogin'
]);
if(!$request->query->has('code')){
// If we don't have an authorization code then get one
$authUrl = $provider->getAuthorizationUrl();
$request->getSession()->set('oauth2state', $provider->getState());
//This doesn't work
return new RedirectResponse($authUrl);
// Check given state against previously stored one to mitigate CSRF attack
}elseif ( empty($request->query->get('state')) || ($request->query->get('state')!==$request->getSession()->get('oauth2state')) ){
return null;
}else{
// Try to get an access token (using the authorization code grant)
$token = $provider->getAccessToken('authorization_code', [
'code' => $request->query->get('code')
]);
try {
//when log with microsoft, check if user is allowed
// We got an access token, let's now get the user's details
$user = $provider->getResourceOwner($token);
} catch (Exception $e) {
// Failed to get user details
}
}
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
return $userProvider->loadUserByUsername($user->getEmail());
}
public function checkCredentials($credentials, UserInterface $user)
{
// check credentials - e.g. make sure the password is valid
// no credential check is needed in this case
// return true to cause authentication success
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$url = $this->router->generate('homepage');
return new RedirectResponse($url);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = array(
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
// or to translate this message
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
);
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $data);
$url = $this->router->generate('login');
return new RedirectResponse($url);
}
Function getCredentials() is not supposed to return a Response, it provide the credentials used in getUser().
In the getUser() documentation :
The credentials are the return value from getCredentials()
You may throw an AuthenticationException if you wish. If you return
null, then a UsernameNotFoundException is thrown for you.
In case of exception thrown, onAuthenticationFailure() is called and here you can return your RedirectResponse.
For more detailled informations, see the source code of the \Symfony\Component\Security\Guard\GuardAuthenticatorInterface which contains a lots of explanations in its methods.
Related
I'm using V1 of https://github.com/tymondesigns/jwt-auth
I need to create an expired token, to test the TokenExpiredException in my code:
public function handle($request, Closure $next)
{
try {
JWTAuth::parseToken()->authenticate();
} catch (Exception $e) {
if ($e instanceof TokenInvalidException) {
return response()->json(['status' => 'Token is Invalid'], 401);
} elseif ($e instanceof TokenExpiredException) {
return response()->json(['status' => 'Token is Expired'], 401);
} else {
return response()->json(['status' => 'Authorization Token not found'], 401);
}
}
return $next($request);
}
I cannot do it:
public function setUp(): void
{
parent::setUp();
$password = '123456';
$user = new User([
'email' => 'info#example.com',
'password' => Hash::make($password),
]);
$user->save();
}
public function testExpiredToken()
{
$user = User::first();
$token = JWTAuth::fromUser($user, ['exp'=> 123456]);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
])->get(Route('test_data_read_closed'));
$response->assertStatus(401);
}
But I get 200 from my test (token accepted, I got answer from my route) and not 401.
How can I create an expired token? Thank you
I spent hours trying to figure out why it was still responding with a 200 success code when an expired JWT is sent (for testing purposes). It turns out that the JWT package caches the claims in the \Tymon\JWTAuth\Factory instance. To fix it, you just have to clear the claims after the JWT is generated and before it's sent to a controller:
\Tymon\JWTAuth\Facades\JWTAuth::getPayloadFactory()->emptyClaims();
Otherwise, it thinks it's the same request and will re-use already built \Tymon\JWTAuth\Claims\Claim instances to decode another JWT. I will see about creating an issue on GitHub.
I am using oauth2-microsoft to develop a 'sign in with Microsoft' tool for my app. I'm successfully authenticating and receiving a token, but then I receive an error from the sample code.
I am using the sample code below and have tried various combinations of URLs in the 'urlResourceOwnerDetails' field, including leaving it blank.
$provider = new \Stevenmaguire\OAuth2\Client\Provider\Microsoft([
'clientId' => '<redacted>',
'clientSecret' => '<redacted>',
'redirectUri' => 'http://localhost/test.php',
'urlAuthorize' => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
'urlAccessToken' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
'urlResourceOwnerDetails' => 'https://graph.microsoft.com/v1.0/me/drive'
]);
$options = [
'scope' => ['wl.basic', 'wl.signin']
];
After this comes authentication and token generation.
Then this line throws errors:
$user = $provider->getResourceOwner($token);
A token is definitely being generated, as I can echo $token and see it.
The above code should create a $user object that contains details about the logged in user. However, instead it generates these errors:
If 'urlResourceOwnerDetails' is set to https://graph.microsoft.com/v1.0/me/drive I get:
League\OAuth2\Client\Provider\Exception\IdentityProviderException: Access token is empty
If 'urlResourceOwnerDetails' is set to https://outlook.office.com/api/v2.0/me I get:
UnexpectedValueException: Invalid response received from Authorization Server. Expected JSON.
And if 'urlResourceOwnerDetails' is empty I get:
GuzzleHttp\Exception\RequestException: cURL error 3: malformed (see http://curl.haxx.se/libcurl/c/libcurl-errors.html)
Any ideas, please?
It appears oauth2-microsoft does not support Microsoft Graph Auth to a full extent at the moment, refer for example this thread
Regarding the error
League\OAuth2\Client\Provider\Exception\IdentityProviderException:
Access token is empty
access token is expected to be passed as Authorization header but according to Microsoft.php provider implementation it is passed instead as query string:
public function getResourceOwnerDetailsUrl(AccessToken $token)
{
$uri = new Uri($this->urlResourceOwnerDetails);
return (string) Uri::withQueryValue($uri, 'access_token', (string) $token);
}
The way how library is designed, the following provider class could be introduced to support Microsoft Graph calls (by including access token in the Authorization header of a request)
class MicrosoftGraphProvider extends AbstractProvider
{
/**
* Get provider url to fetch user details
*
* #param AccessToken $token
*
* #return string
*/
public function getResourceOwnerDetailsUrl(AccessToken $token)
{
return 'https://graph.microsoft.com/v1.0/me';
}
protected function getAuthorizationHeaders($token = null)
{
return ['Authorization'=>'Bearer ' . $token->getToken()];
}
public function getBaseAuthorizationUrl()
{
return 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize';
}
public function getBaseAccessTokenUrl(array $params)
{
return 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
}
protected function getDefaultScopes()
{
return ['openid profile'];
}
protected function checkResponse(\Psr\Http\Message\ResponseInterface $response, $data)
{
// TODO: Implement checkResponse() method.
}
protected function createResourceOwner(array $response, AccessToken $token)
{
return (object)$response;
}
}
Currently I'm developing Laravel 5.8 with using JWT Auth, everything running as well in Postman, but when I tried for testing on Browser, I got a lot of errors and one by one has been fixed. Now I'm get another error when I try to pass JSON Web Token by using Request. The token isn't provided correctly. After I do sign in process in :
public function signin(Request $request)
{
$this->validate($request, [
'username' => 'required',
'password' => 'required'
]);
// grab credentials from the request
$credentials = $request->only('username', 'password');
try {
// attempt to verify the credentials and create a token for the user
if (! $token = JWTAuth::attempt($credentials)) {
return response()->json([
'error' => 'Invalid Credentials, username and password dismatches. Or username may not registered.',
'status' => '401'
], 401);
}
} catch (JWTException $e) {
// something went wrong whilst attempting to encode the token
return response()->json(['error' => 'could_not_create_token'], 500);
}
return response()->json([
'token' => $token
]);
}
The token generated successfully. But when I need the token to another controller, the token generated unsuccessfully, one of example is in this method :
public function index(Request $request)
{
// this will set the token on the object
JWTAuth::parseToken();
// and you can continue to chain methods
$user = JWTAuth::parseToken()->authenticate();
$token = JWTAuth::getToken();
die($token);
try {
if (! $user = JWTAuth::parseToken()->authenticate()) {
return response()->json(['user_not_found'], 404);
}
} catch (Tymon\JWTAuth\Exceptions\TokenExpiredException $e) {
return response()->json(['token_expired'], $e->getStatusCode());
} catch (Tymon\JWTAuth\Exceptions\TokenInvalidException $e) {
return response()->json(['token_invalid'], $e->getStatusCode());
} catch (Tymon\JWTAuth\Exceptions\JWTException $e) {
return response()->json(['token_absent'], $e->getStatusCode());
}
Everytime I'd like to JWTAuth::parseToken(); I got this error :
The token could not be parsed from the request
So why this happen? And what should I do? Because In signin method, the token successfully generated, but in index I can't access the token. Thanks for your attention.
Token needs to be passed via Headers in each api request
Header Name: Authorization
Expected Value: Bearer --token--
(without the -- ofcourse)
I have a Laravel 5 web app with Socialite to login my user by using Facebook account.
This is my callback function:
public function callback(SocialAccountService $service)
{
$user = $service->createOrGetUser(Socialite::driver('facebook')->user());
auth()->login($user);
return redirect()->to('/home');
}
This is my SocialAccountService, basically the function returns the user if existing or creates a new one:
class SocialAccountService
{
public function createOrGetUser(ProviderUser $providerUser)
{
$account = SocialAccount::whereProvider('facebook')
->whereProviderUserId($providerUser->getId())
->first();
if ($account) {
return $account->user;
} else {
$account = new SocialAccount([
'provider_user_id' => $providerUser->getId(),
'provider' => 'facebook'
]);
$user = User::whereEmail($providerUser->getEmail())->first();
if (!$user) {
$user = User::create([
'email' => $providerUser->getEmail(),
'name' => $providerUser->getName(),
]);
}
$account->user()->associate($user);
$account->save();
return $user;
}
}
}
Now the problem is, I am able to make my user login successfully via Facebook, but when the user clicks Cancel on the FB dialog for permission, it breaks.
How can I handle this error? I can see the error message in URL box, but don't know to handle them.
P.S I am fairly new to Laravel and Socialite
in the callback(...) method you can check for presense of the 'code' input field and if it is not there redirect to back to login page with errors.
Example:
function callback(SocialAccountService $service ,Request $request) {
if (! $request->input('code')) {
return redirect('login')->withErrors('Login failed: '.$request->input('error').' - '.$request->input('error_reason'));
}
// rest of your code
}
Try catch is the best practice(it catches all exception)
try
{
$userSocial =Socialite::driver('facebook')
->stateless()->user();
dd($userSocial);
}
catch (\Throwable $e) {
//handle error here for php 7 or 8
} catch (\Exception $e) {
// for php 5
handle error here
}
Here's my security configuration:
security:
firewalls:
secured_area:
pattern: ^/r
stateless: true
simple_preauth:
authenticator: apikey_authenticator
Resources like /r/companies/1 and /r/news/2 need to be accessible by my Backbone.js application without the need of an apikey to be sent along with the request for GET requests. For POST, DELETE and PATCH requests that deal with changing the state of the resource, an apikey must be sent along with the request, the user needs to be logged in thus and get the chance to log in when that’s not the case through a 403 status code.
Right now all requests for resources are triggering the pre authentication process and the problem is that I wasn’t able to configure it to let anonymous users pass. This is just a test, here's the code(based on the tutorial How to Authenticate Users with API Keys):
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationSuccessHandlerInterface
{
protected $userProvider;
protected $httpUtils;
public function __construct(UserProviderInterface $userProvider, HttpUtils $httpUtils)
{
$this->userProvider = $userProvider;
$this->httpUtils = $httpUtils;
}
public function createToken(Request $request, $providerKey)
{
$targetUrl = '/r/login/check';
if ($this->httpUtils->checkRequestPath($request, $targetUrl)){
if (!$request->query->has('email')) {
throw new BadCredentialsException('Credentials not correct or not present');
}
if (!$request->query->has('password')) {
throw new BadCredentialsException('Credentials not correct or not present');
}
if($request->query->get('email') === 'testemail' && $request->query->get('password') === 'testpassword'){
$token = 12345;
}
}else{
if (!$request->query->has('apikey')) {
$token = '';
}else{
$token = $request->query->get('apikey');
}
}
return new PreAuthenticatedToken( //what is the meaning of this class?
'anon.',
$token,
$providerKey
);
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
$apiKey = $token->getCredentials();
$username = $this->userProvider->getUsernameForApiKey($apiKey);
if (!$username) {
throw new AuthenticationException("Apikey not found");
}
$user = $this->userProvider->loadUserByUsername($username);
return new PreAuthenticatedToken(
$user,
$apiKey,
$providerKey,
$user->getRoles()
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
$targetUrl = '/r/login/check';
if ($this->httpUtils->checkRequestPath($request, $targetUrl)){
$response = new Response();
$response->setStatusCode(Response::HTTP_OK);
$response->headers->set('apikey', '12345');
return $response;
}
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
}
Right now this code doesn’t work for GET requests without the apikey query parameter since for this situation $token = '' and therefore no $user will be returned from getUsernameForApiKey()
How to handle this situation? I'd rather have GET requests for ^/r not entering the pre auth process(I don't know how to do that). But if they do, how can I let anonymous users pass it? Right now I keep getting a A Token was not found in the SecurityContext. (500 Internal Server Error). The code is incomplete obviously, but that's because I don't know how to proceed.
Using the class AnonymousToken inside authenticateToken() solved my problem. Although I still don't know if it's safe or the best way of achieving what I wanted.
$apiKey = $token->getCredentials();
if($apiKey == ''){
//request without a token
return new AnonymousToken('anon.','anonymous',array('IS_AUTHENTICATED_ANONYMOUSLY'));
}
Here is the Symfony API reference for the AnonymousToken class.