Building OAuth-based authentication with Angular & Laravel
- Angular
- Laravel
- OAuth 2.0
- authentication
There are a few features in web development world that pretty much every developer will need to implement at some point in their career, and more often than not, many times and in different ways. This article will quite comprehensively go through implementation of the OAuth 2.0 authentication using a quite common tech stack including Angular front-end and Laravel back-end.
There are several ways of handling authentication in web applications and choosing the right one will largely depend on the project architecture. Laravel comes by default with a session based authentication which works great for server-side rendered views. (A traditional MVC architecture) Session based authentication on a fundamental level uses tokens - authentication flow where user provides the username and password in exchange for a temporary token they can use for subsequent requests. Modern web APIs however rely on techniques like JWT and OAuth and the latter is what we’ll be implementing today.
I want to make it very clear that web API authentication, no matter what method you choose, will basically work the same, so the choice is a matter of preference and application requirements. Laravel support for OAuth 2.0 comes in a form of official package called Passport but there’s also a more lightweight solution called Sanctum.
In this article I’m assuming you have at least basic knowledge of both Laravel and Angular.
What is OAuth 2.0?
OAuth is a protocol for authorisation - granting websites and applications access to information. It provides multiple authorisation flows that suit different requirements which in turn improves the developer experience. Using a single protocol you’re able to authenticate web application, mobile clients and give access to external websites. If you’ve ever logged into a web application using your Google or Facebook account, you’re already familiar with OAuth 2.0.
There are two concepts fundamental to OAuth 2.0 flow - access tokens and clients. Access tokens are random strings which the users (like SPA) will provide to the API as a proof of who they are (e.g. a user of the application) and the clients are applications that are attempting to gain access to information
Setting up the back-end
I’ll assume you’ve bootstrapped your Laravel application (e.g. using laravel new
command that comes with a Laravel installer and you have at least one route that needs to be hidden behind an authentication.
As mentioned before, we’ll utilise Laravel Passport package, so first you need to get it installed in your project using composer
:
composer require laravel/passport
It comes with a set of migrations which you need to run:
php artisan migrate
You’ll also need to create encryption keys and clients used to generate tokens:
php artisan passport:install
When you run the command above you’ll see the terminal output containing the ID of the client and the secret string. You can make note of it or you can retrieve it later in the database in oauth_clients
table. What we care about is Password Grant Client credentials.
You now also need to add HasApiToken
trait to your User
model, append Passport::routes();
to the end of boot()
method in AuthServiceProvider
and update the guards in config/auth.php
:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
You can always refer to the official Laravel Passport documentation for more details.
Storing the password grant secret in the front-end is very insecure and should never be done. OAuth 2.0 requires those values to be passed in the authorisation request so you will need to create a proxy endpoint in your API. To do that, create your authentication controller if you haven’t already (e.g. by running php artisan make:controller AuthController
) and add the following method to it:
public function token(Request $request)
{
$request->request->add([
'grant_type' => 'password',
'client_id' => config('auth.passport.client_id'),
'client_secret' => config('auth.passport.client_secret'),
]);
$proxy = Request::create('oauth/token', 'post');
return Route::dispatch($proxy);
}
This is the core of the solution and it’s actually quite simple. It makes use of Laravel’s ability to redispatch a request while appending additional data to it. As you can see, we’re adding the following fields: grant_type
set to password
, client_id
and client_secret
representing the credentials to the client you created before.
Those values will change every time you rerun the migrations so shouldn’t be hardcoded. The controller action above references the config file, so let’s add the following to config/auth.php
:
'passport' => [
'client_id' => env('PASSPORT_CLIENT_ID'),
'client_secret' => env('PASSPORT_CLIENT_SECRET'),
]
Now we only need to add the actual values to the environment file .env
:
PASSPORT_CLIENT_ID=2
PASSPORT_CLIENT_SECRET='{secret from the passport:install command output}'
Client ID 2
is currently the default in Laravel Passport for the password grant client but make sure it matches the one in the database. You can use this handy SQL query to retrieve those credentials:
select id, secret from oauth_clients where password_client;
It feels like too much work to reference the config file first and the environment file from it instead of accessing the value using env()
helper but it’s necessary because of Laravel’s caching mechanism.
Let’s add another method to AuthController
that retrieves the details for the currently logged in user:
public function user()
{
return auth()->user();
}
And finally create routes in routes/api.php
:
Route::post('auth/token', 'AuthController@token');
Route::group(['middleware' => ['auth:api']], function () {
Route::get('auth/user', 'AuthController@user');
});
As you can see we’re protecting the AuthController@user
action behind the API authorisation.
Connecting the front-end
Like in case of the Laravel web API I’m assuming you have Angular application scaffolded and ready. In order to connect the authentication endpoints created before with the single page application, you’ll need to take the following steps:
- create a Guard class and protect the routes that should not be viewed anonymously
- add an Interceptor that injects the access token into http requests
- add another Interceptor that handles the
401 Unauthorized
API response - create a data model and actions in the state management library (like NGXS)
Obviously this is just one solution out of many but it’s worked very well so far on multiple projects, doesn’t take long to implement and is quite flexible.
I don’t make any assumptions about how you organise folder structure in your Angular project but the code examples below will use the CoreModule
pattern which is quite popular in Angular projects. (And one I’d strongly recommend) You can read more about it in this Frontpills blog post.
Firstly, let’s create the necessary classes:
ng g guard core/guards/auth
ng g interceptor core/interceptors/token
ng g interceptor core/interceptors/error
In addition, you’ll need core/store/auth.actions.ts
that could look something like this:
export class SignIn {
static readonly type = '[Auth] Sign In';
constructor(public username: string, public password: string) {}
}
export class UpdateUser {
static readonly type = '[Auth] Update User';
constructor(public user: User) {}
}
And core/store/auth.state.ts
:
// imports omitted for brevity.
export interface AuthStateModel {
accessToken?: string;
user?: User;
}
@State<AuthStateModel>({
name: 'auth',
})
@Injectable()
export class AuthState {
// selectors and constructor ommited
@Action(SignIn)
public signIn({ patchState }: StateContext<AuthStateModel>, { username, password }: SignIn) {
return this.authService.createToken(username, password).pipe(tap(({ access_token: accessToken }) => patchState({ accessToken })));
}
@Action(UpdateUser)
public updateUser({ patchState }: StateContext<AuthStateModel>, { user }: UpdateUser) {
patchState({ user });
}
}
User
class represents the data model returned by /auth/user
API endpoint. Worth noting that the Laravel response keys are formatted in snake_case while JavaScript’s and TypeScript convention is to use camelCase — hence the conversion in the SignIn
action handler above.
You can see the reference to AuthService
here, let’s create it then:
ng g service core/services/auth
And add the relevant method:
public createToken(
username: string,
password: string,
): Observable<OAuthResource> {
return this.http.post<OAuthResource>('/api/auth/token', {
username,
password,
});
}
It’s trivial and as we discussed above, doesn’t leak the client secret.
We can now write our guard class, which can then be used in the routing modules using canActivate: [AuthGuard],
:
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(private router: Router, private store: Store, private userService: UserService) {}
canActivate(next: ActivatedRouteSnapshot, { url }: RouterStateSnapshot): Observable<boolean> | boolean {
const accessToken = this.store.selectSnapshot(AuthState.accessToken);
if (!accessToken) {
this.router.navigateByUrl('/auth/login');
return false;
}
if (!!this.store.selectSnapshot(AuthState.user)) {
return true;
}
return this.userService.current().pipe(
map((user: User) => {
this.store.dispatch(new UpdateUser(user));
return true;
}),
catchError(() => of(false))
);
}
}
This guard handler will run under three possible conditions:
- if the access token is not set, the guard will redirect the user to login page
- if the access token is set but the user is not, it will call
UserService
and try to retrieve the currently logged in user details - in the presence of both access token and user object, the guard will allow the route to be loaded
Very simple and clean. I personally love the ability to return either a literal boolean value or the observable from the canActivate
method in the guard classes.
Let’s write the TokenInterceptor
class now:
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(private store: Store) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const accessToken = this.store.selectSnapshot(AuthState.accessToken);
if (accessToken && !req.url.includes('auth/token')) {
const tokenHeader = 'Bearer ' + accessToken;
req = req.clone({
setHeaders: {
Authorization: tokenHeader,
},
});
}
return next.handle(req);
}
}
Should be pretty self explanatory and the most interesting bit is the usage of req.clone
which is a handy wy of updating request headers in Angular. Also note that the token is only appended when it’s available and when it’s not a log in request.
Finally, we can write the 401 Unauthorized HTTP error interceptor that will handle the situation when the access token is no longer valid.
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private authService: AuthService, private router: Router, private store: Store) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
// Ignore non-401 errors in this interceptor
if (request.url.includes('/auth/token') || error.status !== 401) {
return throwError(error);
}
this.store.dispatch(new LogOut());
this.router.navigateByUrl('/auth/login');
})
);
}
}
And that’s it, you now covered the most important use cases:
- creating a token from a provided username and password
- redirecting unauthenticated users to the login page
- attaching access token to authenticated requests
Next steps
If you were following along you might’ve noticed there’s some code that has been omitted. This is because it was either trivial or completely dependent on your application architecture. This was not supposed to be copy-and-paste ready solution, rather a guide on how to implement authentication in this particular tech stack using the experience I’ve gained working as a full-stack developer.
Some of the bits you may need to review or add yourself are:
- authentication pages (make sure there’s no
AuthGuard
protection on them!) - you’ll want to save the access token into local storage; NGXS Storage plugin can be used for that
- write unit tests of course
If you think you found a mistake or have any question, you can always reach out to me on Twitter - I’m @gkedzierski.
PS. If you liked this article, please share to spread the word.