How to make JWT cookie authentication in Laravel - api

I want to have JWT authentication in Laravel >=5.2, using this (Tymon JWT-auth) library but I want to put JWT token into HttpOnly Cookies - to protect JWT token from steal from XSS attack.
I set up Tymon library and... in project: app/Providers/RouteServiceProvider#mapWebRoutes i deactivate execution 'web' middelware group for all requests (which is default laravel behavior - you can see it by php artisan route:list) by remove 'middleware' => 'web' (If I don't do it, i will see CSRF problem with post request).
in routes.php i write:
Route::group(['middleware' =>'api', 'prefix' => '/api/v1', 'namespace' => 'Api\V1'], function () {
Route::post('/login', 'Auth\AuthController#postLogin');
...
Route::get('/projects', 'ProjectsController#getProjects');
}
In may Api\V1\Auth\AuthController#postLogin i generate token and send it back as httpOnly cookie:
...
try
{
$user = User::where('email','=',$credentials['email'])->first();
if ( !($user && Hash::check($credentials['password'], $user->password) ))
{
return response()->json(['error' => 'invalid_credentials'], 401);
}
$customClaims = ['sub' => $user->id, 'role'=> $user->role, 'csrf-token' => str_random(32) ];
$payload = JWTFactory::make($customClaims);
$token = JWTAuth::encode($payload);
} catch(...) {...}
return response()->json($payload->toArray())->withCookie('token', $token, config('jwt.ttl'), "/", null, false, true);
And, yeah here question starts. I would like to do something (may be modifiy laravel Auth class) on each request:
get coookie from request
decode it
check is right (if not trhow 401)
get user from DB
and make that method Auth::user() works every where like in usual way in laravel (so i can use it in each Controller for example)
Any ideas how to do point 4 ?
UPDATE
I also add here protection for CSRF attack - csrf-token is in JWT, and it is also return in body of response for login request (so JS have acces to this csrf-token) (i return only public part of JWT token in login response, whole JWT is return only in cookie, so it is XSS safe) - then front JS must copy csrf-token into header of each request. Then the middelware JWTAuthentiacate (in my answer below) compare csrf-token header with csrf-token field in JWT payload - if they are similar then request pass csrf test.

You can do it simple by creating middleware.
In handle() method just get cookie from request, decode it and login a user using id with this Laravel method:
Auth::loginUsingId($userIdFromToken);

I implement #ƁukaszKuczmaja idea in this way an it works! :) . So i create file in app/Http/Middleware/JWTAuthenticate.php :
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use JWTAuth;
use Tymon\JWTAuth\Token;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Illuminate\Session\TokenMismatchException;
class JWTAuthenticate
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string|null $guard
* #return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
try {
if(!$request->headers->has('csrf-token')) throw new TokenMismatchException();
$rawToken = $request->cookie('token');
$token = new Token($rawToken);
$payload = JWTAuth::decode($token);
if($payload['csrf-token'] != $request->headers->get('csrf-token')) throw new TokenMismatchException();
Auth::loginUsingId($payload['sub']);
} catch(\Exception $e) {
if( $e instanceof TokenExpiredException) {
// TODO token refresh here
}
return response('Unauthorized.', 401);
}
return $next($request);
}
}
In app\Http\Kernel.php#$routeMiddelware I add line:
'jwt.auth' => \App\Http\Middleware\JWTAuthenticate::class,
My routing file looks like this now:
Route::group(['middleware' =>'api', 'prefix' => '/api/v1', 'namespace' => 'Api\V1'], function () {
Route::post('/login', 'Auth\AuthController#postLogin');
Route::group(['middleware' =>'jwt.auth'], function () {
Route::post('/projects', 'ProjectsController#postProjects');
Route::get('/projects', 'ProjectsController#getProjects');
Route::put('/projects/{project}', 'ProjectsController#putProjects');
Route::delete('/projects/{project}', 'ProjectsController#deleteProjects');
});
});
And for instance in app/Http/Controllers/Api/V1/ProjectsController.php i have:
public function getProjects() {
$uid = Auth::user()->id;
return Project::where('user_id','=',$uid)->get();
}

Actually you can put every route that needs authentication within a route group and add the middleware like this:
Route::group(['middleware' => ['jwt.auth']], function () {
Route::patch('/profile', 'UserController#update');
});
The middleware already does what you wanted so there is no need to write additional logic. Don't use an additional handle method.
Within your i.e. UserController you can then i.e.
$user = \Auth::user();
And i.e. depending what you need...
// assign fields
$user->save();
return 'success'; // or whatever you need
Don't reinvent the wheel and keep things DRY.

Related

Could not complete oAuth2.0 login

I have implemented Aspnet.security.openidconnect.server with .net core 2.1 app. Now I want to test my authorization and for that I am making postman request. If I change the grant type to client_credentials then it works but I want to test complete flow, so I select grant type to Authorzation code and it starts giving error "Could not complete oAuth2.0 login.
Here is the code:
services.AddAuthentication(OAuthValidationDefaults.AuthenticationScheme).AddOAuthValidation()
.AddOpenIdConnectServer(options =>
{
options.AuthorizationEndpointPath = new PathString(AuthorizePath);
// Enable the token endpoint.
options.TokenEndpointPath = new PathString(TokenPath);
options.ApplicationCanDisplayErrors = true;
options.AccessTokenLifetime = TimeSpan.FromMinutes(5);
#if DEBUG
options.AllowInsecureHttp = true;
#endif
options.Provider.OnValidateAuthorizationRequest = context =>
{
if (string.Equals(context.ClientId, Configuration["OpenIdServer:ClientId"], StringComparison.Ordinal))
{
context.Validate(context.RedirectUri);
}
return Task.CompletedTask;
};
// Implement OnValidateTokenRequest to support flows using the token endpoint.
options.Provider.OnValidateTokenRequest = context =>
{
// Reject token requests that don't use grant_type=password or grant_type=refresh_token.
if (!context.Request.IsClientCredentialsGrantType() && !context.Request.IsPasswordGrantType()
&& !context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only grant_type=password and refresh_token " +
"requests are accepted by this server.");
return Task.CompletedTask;
}
if (string.IsNullOrEmpty(context.ClientId))
{
context.Skip();
return Task.CompletedTask;
}
if (string.Equals(context.ClientId, Configuration["OpenIdServer:ClientId"], StringComparison.Ordinal) &&
string.Equals(context.ClientSecret, Configuration["OpenIdServer:ClientSecret"], StringComparison.Ordinal))
{
context.Validate();
}
return Task.CompletedTask;
};
// Implement OnHandleTokenRequest to support token requests.
options.Provider.OnHandleTokenRequest = context =>
{
// Only handle grant_type=password token requests and let
// the OpenID Connect server handle the other grant types.
if (context.Request.IsClientCredentialsGrantType() || context.Request.IsPasswordGrantType())
{
//var identity = new ClaimsIdentity(context.Scheme.Name,
// OpenIdConnectConstants.Claims.Name,
// OpenIdConnectConstants.Claims.Role);
ClaimsIdentity identity = null;
if (context.Request.IsClientCredentialsGrantType())
{
identity = new ClaimsIdentity(new GenericIdentity(context.Request.ClientId, "Bearer"), context.Request.GetScopes().Select(x => new Claim("urn:oauth:scope", x)));
}
else if (context.Request.IsPasswordGrantType())
{
identity = new ClaimsIdentity(new GenericIdentity(context.Request.Username, "Bearer"), context.Request.GetScopes().Select(x => new Claim("urn:oauth:scope", x)));
}
// Add the mandatory subject/user identifier claim.
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
// By default, claims are not serialized in the access/identity tokens.
// Use the overload taking a "destinations" parameter to make sure
// your claims are correctly inserted in the appropriate tokens.
identity.AddClaim("urn:customclaim", "value",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new Microsoft.AspNetCore.Authentication.AuthenticationTicket(
new ClaimsPrincipal(identity),
new Microsoft.AspNetCore.Authentication.AuthenticationProperties(),
context.Scheme.Name);
// Call SetScopes with the list of scopes you want to grant
// (specify offline_access to issue a refresh token).
ticket.SetScopes(
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess);
context.Validate(ticket);
}
return Task.CompletedTask;
};
and here is the postman collection:
Now I am not sure that whether the issue is in my code or in postman collection? I think the callback url is creating some issue but I am not sure. Any help?
Update:
By visiing this page https://kevinchalet.com/2016/07/13/creating-your-own-openid-connect-server-with-asos-implementing-the-authorization-code-and-implicit-flows/ I have found the issue. I haven't handled authorization code flow in my code but I even don't want to. Is there any way I test my code with Resource owner password? I can't see this grant type in request form. In simple words I want postman to open login screen which is in Controller/Login/Index and I select my ssl Certificate and it generates a token for me?
hello i think that you have to add https://www.getpostman.com/oauth2/callback as the redirect_url in your server config, i don't think that your STS server will return tokens back to a non trusted url. that's why it works from your app but not from Postman

Basic authentication in Lumen

I have already written an API call for check authentication using Laravel. I need to move that controller to Lumen for use it as micro service.
This is my controller in Laravel.
public function byCredantial(Request $request)
{
$user = [
'email' => $request->input('email'),
'password' => $request->input('password')
];
if (Auth::attempt($user)) {
$response = $this->getSuccess(Auth::user()->id);
return response()->json($response, 200);
} else {
$response = $this->getError($user);
return response()->json($response, 401);
}
}
Lumen doc is not provide how to do such authentication. They has not function for check creadential is correct. How can i do this in Lumen. Is this possible?
You can do this in Lumen. Facades are disabled by default (if you want to enable it you can see the instructions in the documentation), but I would not recommend enabling the facades as the add additional overhead to your application. Instead, I would modify your function to call app('auth'). This will return the class that the Auth facade proxies without loading all the other facades.
public function byCredantial(Request $request)
{
$user = [
'email' => $request->input('email'),
'password' => $request->input('password')
];
$auth = app('auth');
if ($auth->attempt($user)) {
$response = $this->getSuccess($auth->user()->id);
return response()->json($response, 200);
} else {
$response = $this->getError($user);
return response()->json($response, 401);
}
}
Also, I would recommend reading the authentication documentation and placing the bulk of this code in the AuthServiceProvider.

Laravel 5 API not allowing access to new Angular2 app

I have a Laravel API which is now allowing a new Angular2 application I am building to have access to itself. I get the following error:
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3073' is therefore not allowed access. The response had HTTP status code 404.
This is confusing me quite a bit as I already have two applications (one also an Angular2) application that is communicating with the API fine with no issues whatsoever. I have also created CORS middleware in the API which provides the appropriate headers to allows these applications through.
CORS Middleware
<?php
namespace App\Http\Middleware;
use Closure;
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
class CORS {
public function handle($request, Closure $next) {
header("Access-Control-Allow-Origin: *");
$headers = [
'Access-Control-Allow-Methods'=> 'POST, GET, OPTIONS, PUT, DELETE',
'Access-Control-Allow-Headers'=> 'Content-Type, X-Auth-Token, Origin'
];
if($request->getMethod() == "OPTIONS") {
// The client-side application can set only headers allowed in Access-Control-Allow-Headers
return Response::make('OK', 200, $headers);
}
$response = $next($request);
foreach($headers as $key => $value) {
$response->header($key, $value);
}
return $response;
}
}
My Angular2 application is attempting to call my API using this code:
import { Injectable } from "#angular/core";
import { Http, Headers, RequestOptions } from "#angular/http";
import { Observable } from "rxjs";
#Injectable()
export class AuthService {
constructor(private _http: Http) {
}
login(loginDetails):Observable {
let body = JSON.stringify(loginDetails);
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this._http.post('http://#####-brain.dev/api/v1', body, options)
.map(res => res.json());
}
}
Does anyone have any advice as to why this particular application is not receiving the Access-Control-Allow-Origin header header? I have put a breakpoint within my CORS middleware and it doesn't hit it at all which is bizarre as it is calling exactly the same endpoint that my other application is and that is working fine.
I have also noticed that it is only POST requests that it doesn't allow through.
Edit: Here is my app/Http/Kernel.php file in the Laravel API:
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'cors' => 'App\Http\Middleware\CORS',
'jwt.auth' => \Tymon\JWTAuth\Middleware\GetUserFromToken::class,
'jwt.refresh' => \Tymon\JWTAuth\Middleware\RefreshToken::class
];
Thanks!

Custom Authentication AngularFire2 Ionic2

I update my application from Ionic 1 to Ionic 2.
For the first App (Ionic 1) I use AngularFire and custom authentication (with Slim Framework). With Ionic 2 I try to do the same thing with AngularFire2 (and firebase 2.4.2) but I have this error when I auth to firebase.
Code (App.ts):
#App({
templateUrl: './build/app.html',
providers: [
FIREBASE_PROVIDERS,
defaultFirebase('https://<APP>.firebaseio.com/'),
firebaseAuthConfig({
provider: AuthProviders.Custom,
method: AuthMethods.CustomToken
})
]
})
Code (Login.ts):
export class LoginPage {
n_adherent:number = null;
password:string = '';
constructor(..., private af:AngularFire, private _authService:AuthService) {}
connect() {
let credentials = {
n_adherent: parseInt(this.n_adherent, 10),
password: this.password
};
// Send credentials to my PHP server
this._autService.login(credentials)
.subscribe(data => {
if (data.token) {
// I get the token
let token = data.token;
// Authenticate to Firebase
this.af.auth.login(token)
.then((data) => console.log(data))
.catch((error) => console.log(error));
}
});
}
}
Error (in console):
You must include credentials to use this auth method.
Code from firebase/php-jwt:
<?php
use \Firebase\JWT\JWT;
$key = "example_key";
$token = array(
"iss" => "http://example.org",
"aud" => "http://example.com",
"iat" => 1356999524,
"nbf" => 1357000000
);
/**
* IMPORTANT:
* You must specify supported algorithms for your application. See
* https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
* for a list of spec-compliant algorithms.
*/
$jwt = JWT::encode($token, $key);
$decoded = JWT::decode($jwt, $key, array('HS256'));
print_r($decoded);
/*
NOTE: This will now be an object instead of an associative array. To get
an associative array, you will need to cast it as such:
*/
$decoded_array = (array) $decoded;
/**
* You can add a leeway to account for when there is a clock skew times between
* the signing and verifying servers. It is recommended that this leeway should
* not be bigger than a few minutes.
*
* Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef
*/
JWT::$leeway = 60; // $leeway in seconds
$decoded = JWT::decode($jwt, $key, array('HS256'));
?>
Your help is needed.
Your token is a string, right?
We just had the same error and had to debug the source code.
We realised, that the this.af.auth.login method is waiting for two parameters.
Simply put, use the following:
this.af.auth.login(token, {})
Cheers,
Marcell
This should fix your issue:
this.af.auth.login(data.token, {
provider: AuthProviders.Custom,
method: AuthMethods.CustomToken
})
.then(data => {
console.log(data);
})
.catch(error => {
console.log(error);
});
Cheers.

Intercept HTTP Request that needs authorization Header with AngularJS ngResource

Hello I'm doing a REST API client with AngularJS using ngResource plugin and my implementation of HMAC authentication.
I wrote an HttpIntercept Service that intercepts the http requests and calculate and attach the Authorization Header with HMAC sign. But with this implementation it calculates and attaches the sign to all requests, that's bad.
.factory('authInterceptor', function($q) {
return {
request: function(request) {
#sign calculation...
request.headers['Authorization'] = sign;
}
return request || $q.when(request);
}
};
})
.controller('HomeCtrl', function ($scope,$resource) {
var Articles = $resource('/api/articles');
$scope.articles = Articles.query();
})
Do you have a suggestion to intercept only requests that needs authentication or all requests that came from ngResource plugin?
I thought to three workrounds:
1. an array list of the private requests
2. different subdomain for public and private APIs
3. attach supply http Header to the requests that need authentication
See $http and overriding transformations and also $resource
Each $resource action takes an $http.config like object which has transformRequest:
var Articles = $resource(
'/api/articles',
{
},
{
'query': {
method: 'GET',
isArray: true,
transformRequest: function (config) {
config.headers['Authentication']: 'sign';
return config;
}
}
});