Authentification Sociale
In addition to traditional password authentication, Foal provides services to authenticate users through social providers. The framework officially supports the following:
- Github
If your provider is not listed here but supports OAuth 2.0, then you can still extend the AbstractProvider
class to integrate it or use a community provider below.
Get Started
General overview
The authentication process works as follows:
- The user clicks the Log In with xxx button in the browser and the client sends a request to the server.
- The server redirects the user to the consent page where they are asked to give permission to log in with their account and/or give access to some of their personal information.
- The user approves and the consent page redirects the user with an authorization code to the redirect URI specified in the configuration.
- Your application then makes one or more requests to the OAuth servers to obtain an access token and information about the user.
- The social provider servers return this information.
- Finally, your server-side application logs the user in based on this information and redirects the user when done.
This explanation of OAuth 2 is intentionally simplified. It highlights only the parts of the protocol that are necessary to successfully implement social authentication with Foal. But the framework also performs other tasks under the hood to fully comply with the OAuth 2.0 protocol and it adds security protection against CSRF attacks.
Registering an application
To set up social authentication with Foal, you first need to register your application to the social provider you chose (Google, Facebook, etc). This can be done through its website.
Usually your are required to provide:
- an application name,
- a logo,
- and redirect URIs where the social provider should redirect the users once they give their consent on the provider page (ex:
http://localhost:3001/signin/google/callback
,https://example.com/signin/google/callback
).
Once done, you should receive:
- a client ID that is public and identifies your application,
- and a client secret that must not be revealed and can therefore only be used on the backend side. It is used when your server communicates with the OAuth provider's servers.
You must obtain a client secret. If you do not have one, it means you probably chose the wrong option at some point.
Installation and configuration
Once you have registered your application, install the appropriate package.
npm install @foal/social
Then, you will need to provide your client ID, client secret and your redirect URIs to Foal. This can be done through the usual configuration files.
- YAML
- JSON
- JS
settings:
social:
google:
clientId: 'xxx'
clientSecret: 'env(SETTINGS_SOCIAL_GOOGLE_CLIENT_SECRET)'
redirectUri: 'http://localhost:3001/signin/google/callback'
{
"settings": {
"social": {
"google": {
"clientId": "xxx",
"clientSecret": "env(SETTINGS_SOCIAL_GOOGLE_CLIENT_SECRET)",
"redirectUri": "http://localhost:3001/signin/google/callback"
}
}
}
}
module.exports = {
settings: {
social: {
google: {
clientId: 'xxx',
clientSecret: 'env(SETTINGS_SOCIAL_GOOGLE_CLIENT_SECRET)',
redirectUri: 'http://localhost:3001/signin/google/callback'
}
}
}
}
.env
SETTINGS_SOCIAL_GOOGLE_CLIENT_SECRET=yyy
Adding controllers
The last step is to add a controller that will call methods of a social service to handle authentication. The example below uses Google as provider.
// 3p
import { Context, dependency, Get } from '@foal/core';
import { GoogleProvider } from '@foal/social';
export class AuthController {
@dependency
google: GoogleProvider;
@Get('/signin/google')
redirectToGoogle() {
// Your "Login In with Google" button should point to this route.
// The user will be redirected to Google auth page.
return this.google.redirect();
}
@Get('/signin/google/callback')
async handleGoogleRedirection(ctx: Context) {
// Once the user gives their permission to log in with Google, the OAuth server
// will redirect the user to this route. This route must match the redirect URI.
const { userInfo, tokens } = await this.google.getUserInfo(ctx);
// Do something with the user information AND/OR the access token.
// If you only need the access token, you can call the "getTokens" method.
// The method usually ends with a HttpResponseRedirect object as returned value.
}
}
You can also override in the redirect
method the scopes you want:
return this.google.redirect({ scopes: [ 'email' ] });
Additional parameters can passed to the redirect
and getUserInfo
methods depending on the provider.
Techniques
Usage with sessions
This example shows how to manage authentication (login and registration) with sessions.
user.entity.ts
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
}
export { DatabaseSession } from '@foal/typeorm';
auth.controller.ts
// 3p
import {
Context,
dependency,
Get,
HttpResponseRedirect,
Store,
UseSessions,
} from '@foal/core';
import { GoogleProvider } from '@foal/social';
import { User } from '../entities';
export class AuthController {
@dependency
google: GoogleProvider;
@dependency
store: Store;
@Get('/signin/google')
redirectToGoogle() {
return this.google.redirect();
}
@Get('/signin/google/callback')
@UseSessions({
cookie: true,
})
async handleGoogleRedirection(ctx: Context<User>) {
const { userInfo } = await this.google.getUserInfo<{ email: string }>(ctx);
if (!userInfo.email) {
throw new Error('Google should have returned an email address.');
}
let user = await User.findOneBy({ email: userInfo.email });
if (!user) {
// If the user has not already signed up, then add them to the database.
user = new User();
user.email = userInfo.email;
await user.save();
}
ctx.session!.setUser(user);
return new HttpResponseRedirect('/');
}
}
Usage with JWT
This example shows how to manage authentication (login and registration) with JWT.
user.entity.ts
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
}
auth.controller.ts
// std
import { promisify } from 'util';
// 3p
import {
Context,
dependency,
Get,
HttpResponseRedirect,
} from '@foal/core';
import { GoogleProvider } from '@foal/social';
import { getSecretOrPrivateKey, setAuthCookie } from '@foal/jwt';
import { sign } from 'jsonwebtoken';
import { User } from '../entities';
export class AuthController {
@dependency
google: GoogleProvider;
@Get('/signin/google')
redirectToGoogle() {
return this.google.redirect();
}
@Get('/signin/google/callback')
async handleGoogleRedirection(ctx: Context) {
const { userInfo } = await this.google.getUserInfo<{ email: string }>(ctx);
if (!userInfo.email) {
throw new Error('Google should have returned an email address.');
}
let user = await User.findOneBy({ email: userInfo.email });
if (!user) {
// If the user has not already signed up, then add them to the database.
user = new User();
user.email = userInfo.email;
await user.save();
}
const payload = {
email: user.email,
id: user.id,
};
const jwt = await promisify(sign as any)(
payload,
getSecretOrPrivateKey(),
{ subject: user.id.toString() }
);
const response = new HttpResponseRedirect('/');
await setAuthCookie(response, jwt);
return response;
}
}
Custom Provider
If your provider is not officially supported by Foal but supports the OAuth 2.0 protocol, you can still implement your own social service. All you need to do is to make it inherit from the AbstractProvider
class.
Example
// 3p
import { AbstractProvider, SocialTokens } from '@foal/core';
export interface GithubAuthParameter {
// ...
}
export interface GithubUserInfoParameter {
// ...
}
export class GithubProvider extends AbstractProvider<GithubAuthParameter, GithubUserInfoParameter> {
protected configPaths = {
clientId: 'social.github.clientId',
clientSecret: 'social.github.clientSecret',
redirectUri: 'social.github.redirectUri',
};
protected authEndpoint = '...';
protected tokenEndpoint = '...';
protected userInfoEndpoint = '...'; // Optional. Depending on the provider.
protected defaultScopes: string[] = [ 'email' ]; // Optional
async getUserInfoFromTokens(tokens: SocialTokens, params?: GithubUserInfoParameter) {
// ...
// In case the server returns an error when requesting
// user information, you can throw a UserInfoError.
}
}
Sending the Client Credentials in an Authorization Header
When requesting the token endpoint, the provider sends the client ID and secret as a query parameter by default. If you want to send them in an Authorization
header using the basic scheme, you can do so by setting the useAuthorizationHeaderForTokenEndpoint
property to true
.
Enabling Code Flow with PKCE
If you want to enable code flow with PKCE, you can do so by setting the usePKCE
property to true
.
By default, the provider will perform a SHA256 hash to generate the code challenge. If you wish to use the plaintext code verifier string as code challenge, you can do so by setting the
useCodeVerifierAsCodeChallenge
property totrue
.
When using this feature, the provider encrypts the code verifier and stores it in a cookie on the client. In order to do this, you need to provide a secret using the configuration key settings.social.secret.codeVerifierSecret
.
- YAML
- JSON
- JS
settings:
social:
secret:
codeVerifierSecret: 'xxx'
{
"settings": {
"social": {
"secret": {
"codeVerifierSecret": "xxx"
}
}
}
}
module.exports = {
settings: {
social: {
secret: {
codeVerifierSecret: 'xxx'
}
}
}
}
Official Providers
Google
Service name | Default scopes | Available scopes |
---|---|---|
GoogleProvider | openid , profile , email | Google scopes |
Register an OAuth application
Visit the Google API Console to obtain a client ID and a client secret.