Lewati ke konten utama
Versi: 4.x

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
  • Twitter

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.

// 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 to true.

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.

settings:
social:
secret:
codeVerifierSecret: 'xxx'

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.

Twitter

Service nameDefault scopesAvailable scopes 
TwitterProviderusers.read tweet.readAPI documentation

Register an OAuth application

Visit this page to create an application and obtain a client ID and a client secret. You must configure Oauth2 settings to be used with public client;

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.
CodeVerifierNotFoundThe encrypted code verifier was not found in the cookie (only when using PKCE).
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'