Open 0x7357 opened 3 years ago
Thanks for raising this. The reason I haven't done something like this is it is really dependent on how you authenticate users to your API, which can vary drastically from app to app. E.g. are you using tokens? are you use Sanctum for an SPA? Etc etc. Not only does it depend on how you want to do it in the backend (the Laravel app), it also depends on how you expect API clients to authenticate, which can vary from app to app. You may for example need to support multiple authentication strategies in complex applications.
The reality is there's lots of different approaches you can use, so I don't think there's a definitive answer. Which makes adding it to the demo app difficult because I wouldn't want to recommend one approach over any other approach.
Legit answer. But since the API seems to be an encapsulated environment, Auth does not seem to be a part of the jsonapi itself. And a "best practice hint" would be awesome (at least for me).
/api/v1/auth
or /api/auth
? I'd say the 2nd example seems to be more logical because you could authenticate yourself via /api/auth
for /api/v2/...
, too.
Edit:
You are using $request->user()
(or similar in your dummy app) and this already seems to be a result of something like Auth. Where does the user entity comes from?
Yeah so we use Laravel Fortify - which adds routes that are set up both for HTML and JSON requests, e.g. at the /login
endpoint. So we just use those as it meets our requirements and keeps the login/logout capability unified (rather than adding a seperate API login). In this scenario I wouldn't recommend writing a JSON:API resource for login, because submitting login credentials doesn't really constitute a resource - so that's why we don't build JSON:API specific login routes. We do however add a route that returns the JSON:API users
resource that represents the current logged in user.
I'd do it differently though if I was using some sort of token authentication that was specific to the API. Then I would use something like /api/auth
endpoints that are specific to authenticating to the API. I might be tempted to have a JSON:API auth-tokens
resource for which a POST
request created a token. But haven't actually had to build anything like that yet as none of the production apps I work on have done authentication like that.
It's the kind of thing that would be good for a blog - describing different approaches. At the moment though I just don't have time to do something like that! (Time is always my enemy!)
I have taken a short look to Fortify and Sanctum. I usually prefer JWT, but it seems JWT isn't as common as API Keys. I like the approach, that API keys' permissions is stored in the database, but a JWT already can contain all the permissions. Do you know any solution for the use of JWT and JSON:API?
It totally depends what you're expecting for the API client. In our case it's a SPA so Sanctum is the perfect fit.
For JWT I've previously used this: https://github.com/tymondesigns/jwt-auth
Can you show me how you've set up Sanctum with a JSON Api? Sanctum kinda fits to my needs and I've currently read bad stuff about JWT. :-D
@DannyEndert sorry don't have a lot of time to spend sharing all the code, particularly as the application is not open source.
Effectively I authenticate using the /login
route added by Fortify. Then for Sanctum, I just use auth:sanctum
middleware for the API routes. Like this:
JsonApiRoute::server('dancer@v1')
->prefix('v1')
->name('v1.')
->withoutMiddleware(SubstituteBindings::class)
->middleware('auth:sanctum')
->resources(function ($server) {
// API resource routes here
});
It's as straightforward as that. If you're stuck on something specific, let me know.
I like to raise this question again since it has not be fully answered for me. I currently try to build the following: send username and password to /api/v1/access-tokens
and like to get back an access token. Yes, I know that those are two different resource schemas and that is part of the problem. Lately I tried to create an additional resource type LoginCredential
with username
, password
and id
in the schema. But then I have a different route url.
Any idea how I can solve this? Or any way I can change the resource type/schema on the fly? I used the last three days to work through the code but haven't find any solution for now.
@MeiKatz - I'm sure you've solved this by now, but my solution was to create a custom controller and reply with custom JSONAPI responses.
This is my controller in app\Http\Controllers\Api\V1\AuthController.php
:
<?php
namespace App\Http\Controllers\Api\v1;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Validator;
use LaravelJsonApi\Core\Exceptions\JsonApiException;
use LaravelJsonApi\Core\Responses\DataResponse;
use LaravelJsonApi\Core\Responses\MetaResponse;
class AuthController extends Controller
{
public function login(Request $request)
{
$validator = Validator::make($request->all(), [
'data.attributes.email' => 'required|email',
'data.attributes.password' => 'required|min:8',
'data.meta.device_name' => 'required',
]);
if ($validator->fails()) {
return app(\LaravelJsonApi\Validation\Factory::class)
->createErrors($validator);
}
$user = User::where('email', $request->input('data.attributes.email'))->first();
if (! $user || ! Hash::check($request->input('data.attributes.password'), $user->password)) {
throw JsonApiException::error([
'status' => 400,
'detail' => 'The provided credentials are incorrect.',
]);
}
return DataResponse::make($user)->withServer('v1');
}
public function forgot(Request $request)
{
$validator = Validator::make($request->all(), [
'data.attributes.email' => 'required|email',
]);
if ($validator->fails()) {
return app(\LaravelJsonApi\Validation\Factory::class)
->createErrors($validator);
}
if ($user = User::where('email', $request->input('data.attributes.email'))->first()) {
$token = Password::createToken($user);
$user->sendPasswordResetNotification($token);
}
return response()->json([
'jsonapi' => [
'version' => '1.0',
],
])->header('Content-Type', 'application/vnd.api+json');
}
public function reset(Request $request)
{
$validator = Validator::make($request->all(), [
'data.attributes.email' => 'required|email',
'data.attributes.password' => 'required|min:8',
'data.meta.device_name' => 'required',
]);
if ($validator->fails()) {
return app(\LaravelJsonApi\Validation\Factory::class)
->createErrors($validator);
}
$status = Password::reset(
[
'email' => $request->input('data.attributes.email'),
'password' => $request->input('data.attributes.password'),
'token' => $request->bearerToken(),
],
function ($user, $password) {
$user->update(['password' => Hash::make($password)]);
event(new PasswordReset($user));
}
);
if ($status !== Password::PASSWORD_RESET) {
throw JsonApiException::error([
'status' => 400,
'detail' => 'The provided credentials are incorrect.',
]);
}
$user = User::where('email', $request->input('data.attributes.email'))->first();
return DataResponse::make($user)->withServer('v1');
}
}
and here are my routes in app/routes/api.php
:
Route::prefix('/v1')->group(function () {
Route::post('/auth/login', [AuthController::class, 'login']);
Route::post('/auth/forgot', [AuthController::class, 'forgot']);
Route::post('/auth/reset', [AuthController::class, 'reset']);
});
In my app\JsonApi\V1\Users\UserResourse.php
file I added the following meta
so the login will reply with a token:
public function meta($request): iterable
{
if ($request->method() === 'POST') {
return [
'token' => $this->createToken('app')->plainTextToken,
];
}
return [];
}
This also allows me to send back a token on User
creation which is done through a normal JsonApi endpoint.
@asugai thanks, it's was very usefull
I'd love to see an implementation of user authentication in your dummy app (I did some research and in 2018 you already wanted to add an authentication to the demo app).
Since I'm quite new to JSON APIs and usually love "logical and 'correct' stuff" and since you seem to know anything about JSON APIs... I beg you: help me here, please: . :-)
(And don't forget: you already added authorization to the dummy app - I guess it isn't far away from having a simple and working authentication.)
Thank you!