Skip to main content
Version: 2.6.0 (latest)

Social Authentication

In addition to traditional password authentication, Foal provides services to authenticate users through social providers. The framework officially supports the following:

  • Google
  • Facebook
  • Github
  • Linkedin

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#

Social auth schema

The authentication process works as follows:

  1. The user clicks the Log In with xxx button in the browser and the client sends a request to the server.
  2. 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.
  3. The user approves and the consent page redirects the user with an authorization code to the redirect URI specified in the configuration.
  4. Your application then makes one or more requests to the OAuth servers to obtain an access token and information about the user.
  5. The social provider servers return this information.
  6. 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.

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.

// 3pimport { 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

// 3pimport {  Context,  dependency,  Get,  HttpResponseRedirect,  Session,  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, Session>) {    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.findOne({ 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

// stdimport { promisify } from 'util';
// 3pimport {  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.findOne({ 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

// 3pimport { 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.  }
} 

Official Providers#

Google#

Service nameDefault scopesAvailable scopes
GoogleProvideropenid, profile, emailGoogle scopes

Register an OAuth application#

Visit the Google API Console to obtain a client ID and a client secret.

Redirection parameters#

The redirect method of the GoogleProvider accepts additional parameters. These parameters and their description are listed here and are all optional.

Example

this.google.redirect({ /* ... */ }, {  access_type: 'offline'})

Facebook#

Service nameDefault scopesAvailable scopes
FacebookProvideremailFacebook permissions

Register an OAuth application#

Visit Facebook's developer website to create an application and obtain a client ID and a client secret.

Redirection parameters#

The redirect method of the FacebookProvider accepts an additional auth_type parameter which is optional.

Example

this.facebook.redirect({ /* ... */ }, {  auth_type: 'rerequest'});
NameTypeDescription
auth_type'rerequest'If a user has already declined a permission, the Facebook Login Dialog box will no longer ask for this permission. The auth_type parameter explicity tells Facebook to ask the user again for the denied permission.

User info parameters#

The getUserInfo and getUserInfoFromTokens methods of the FacebookProvider accept an additional fields parameter which is optional.

Example

const { userInfo } = await this.facebook.getUserInfo(ctx, {  fields: [ 'email' ]})
NameTypeDescription
fieldsstring[]List of fields that the returned user info object should contain. These fields may or may not be available depending on the permissions (scopes) that were requested with the redirect method. Default: ['id', 'name', 'email'].

Github#

Service nameDefault scopesAvailable scopes
GithubProvidernoneGithub scopes

Register an OAuth application#

Visit this page to create an application and obtain a client ID and a client secret.

Additional documentation on Github's redirect URLs can be found here.

Redirection parameters#

The redirect method of the GithubProvider accepts additional parameters. These parameters and their description are listed below and are all optional.

Example

this.github.redirect({ /* ... */ }, {  allow_signup: false})
NameTypeDescription
loginstringSuggests a specific account to use for signing in and authorizing the app.
allow_signupbooleanWhether or not unauthenticated users will be offered an option to sign up for GitHub during the OAuth flow. The default is true. Use false in the case that a policy prohibits signups.

Source: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#parameters

LinkedIn#

Service nameDefault scopesAvailable scopes 
LinkedInProviderr_liteprofileAPI documentation

Register an OAuth application#

Visit this page to create an application and obtain a client ID and a client secret.

User info parameters#

The getUserInfo and getUserInfoFromTokens methods of the LinkedInProvider accept an additional projection parameter which is optional.

Example

const { userInfo } = await this.linkedin.getUserInfo(ctx, {  fields: [ 'id', 'firstName' ]})
NameTypeDescription
fieldsstring[]List of fields that the returned user info object should contain. Additional documentation on field projections.
projectionstringLinkedIn projection parameter.

Community Providers#

There are no community providers available yet! If you want to share one, feel free to open a PR on Github.

Common Errors#

ErrorDescription
InvalidStateErrorThe state query does not match the value found in the cookie.
AuthorizationErrorThe authorization server returns an error. This can happen when a user does not give consent on the provider page.
UserInfoErrorThrown in AbstractProvider.getUserFromTokens if the request to the resource server is unsuccessful.

Security#

HTTPS#

When deploying the application, you application must use HTTPS.

production.yml

settings:  social:    cookie:      # Only pass the state cookie if the request is transmitted over a secure channel (HTTPS).      secure: true    google:      # Your redirect URI in production      redirectUri: 'https://example.com/signin/google/callback'