Security News
Weekly Downloads Now Available in npm Package Search Results
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.
@poppinss/oauth-client
Advanced tools
A framework agnostic package to implement "Login with" flow using OAuth compliant authorization servers.
A framework agnostic package to implement "Login with" flow using OAuth compliant authorization servers.
This package ships with the implementation of OAuth1.0 - three-legged flow
and OAuth2.0 - Authorization Code Grant
flows. You can use it to build "Login with" flow in your Node.js applications.
The motivation for this package is to have a framework-agnostic implementation for the protocols themselves. The passportjs ecosystem relies heavily on the Express framework.
I intentionally decided to only cover OAuth1.0 - three-legged flow and OAuth2.0 - Authorization Code Grant. It keeps the surface area for the code smaller, and these are the two most popular flows for server-side implementations.
There is another protocol-specific package oauth that I have personally used for years but had the following issues with it.
@poppinss/oauth-client
implementation gives you access to the underlying requests, and you have complete freedom to modify the request params.oauth
package was a pain. Maybe it was not written to be extended the way I wanted.The ideal target user for this module is the package creators - someone who wants a low-level framework-agnostic implementation of the Protocols and builds the specialized drivers themselves.
With that said, you can also use this package inside your application code directly. The API is small and offers to tweak almost every request parameter.
Install the package from the npm registry as follows:
npm i @poppinss/oauth-client
## Yarn users
yarn add @poppinss/oauth-client
Make sure to create the .env
file and define all the following variables for the examples to work.
PORT=3000
# Twitter credentials
TWITTER_API_KEY=
TWITTER_APP_SECRET=
# Github credentials
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
# Google credentials
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Gitlab credentials
GITLAB_CLIENT_ID=
GITLAB_CLIENT_SECRET=
This section covers the usage of the generic drivers directly within your application code. Checkout creating custom drivers section to create a custom OAuth2.0 driver.
Oauth2Client
import { Oauth2Client } from '@poppinss/oauth-client/oauth2'
const client = new Oauth2Client({
/**
* The callback registered with Github
*/
callbackUrl: 'http://localhost/github/callback',
/**
* The github client id. Keep it inside env variables
*/
clientId: process.env.GITHUB_CLIENT_ID!,
/**
* The github client secret. Keep it inside env variables
*/
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
/**
* The URL to redirect to for user authorization
*/
authorizeUrl: 'https://github.com/login/oauth/authorize',
/**
* The URL to fetch the access token
*/
accessTokenUrl: 'https://github.com/login/oauth/access_token',
})
The getRedirectUrl
defines the redirect_uri
and the client_id
query params by reading them from your supplied config.
For any other query params, you can pass a callback and modify the request object manually. In the following example, we define the Github specific allow_signup
param.
const url = client.getRedirectUrl((request) => {
/**
* A github specific query string
*/
request.param('allow_signup', true)
})
Based upon your underlying web framework, you must redirect the user to the URL generated in Step 2.
After the user authorizes or denies the login request, the authorization server will redirect them back to your registered callback URL.
Upon redirect, you will get the authorization code
or the error based on the user action. You must read the provider docs and handle the errors correctly before generating an access token.
The getAccessToken
method sets the following form fields for the access token POST request.
You must set the authorization code and any other form fields manually by defining the optional callback.
const accessToken = await client.getAccessToken((request) => {
request.param('code', req.query.code)
})
The generated access token has the following parameters.
expiresIn
exists.All other response values are merged into the accessToken
object, and you can access them directly. For example:
accessToken.scopes
accessToken.idToken
This section covers the usage of the generic drivers directly within your application code. Checkout creating custom drivers section to create a custom OAuth1.0 driver.
Oauth1Client
import { Oauth1Client } from '@poppinss/oauth-client/oauth1'
const client = new Oauth1Client({
/**
* The callback registered with Twitter
*/
callbackUrl: 'http://localhost/twitter/callback',
/**
* The twitter consumer key. Keep it inside env variables
*/
clientId: process.env.GITHUB_CLIENT_ID!,
/**
* The twitter consumer secret. Keep it inside env variables
*/
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
/**
* The URL to generate the initial request token and secret.
*/
requestTokenUrl: 'https://api.twitter.com/oauth/request_token',
/**
* The URL to redirect to for user authorization
*/
authorizeUrl: 'https://api.twitter.com/oauth/authenticate',
/**
* The URL to fetch the access token
*/
accessTokenUrl: 'https://api.twitter.com/oauth/access_token',
})
OAuth1.0 is a three-legged process and requires you to generate a request token and secret before redirecting the user.
const { token, secret } = await client.getRequestToken()
The getRequestToken
method usually doesn't need any extra params. However, do check the provider docs and use the callback to configure the API request.
await client.getRequestToken((request) => {
request.param('key', 'value')
})
The next step is to redirect the user to authorize the request. However, with OAuth1.0, you will have to store the token
and secret
generated in Step 2 inside cookies. Later, you will need it for validation.
const url = client.getRedirectUrl((request) => {
/**
* Set the "oauth_token" generated in Step 2
*/
request.param('oauth_token', token)
})
After the user authorizes or denies the login request, the authorization server will redirect them back to your registered callback URL.
Upon redirect, you will get the oauth_token
and the oauth_verifier
inside the query string. If both or one is missing, then you must abort the request.
Next, you must retrieve the token
and secret
you saved inside the cookies in Step 3. Finally, we will end up with the following four values.
token
: The token value from the cookie (Need it to generate access token)secret
: The token secret the cookie (Need it to generate access token)oauth_token
: Passed as query string by the authorization serveroauth_verifier
: Passed as query string by the authorization serverVerify the token
and the oauth_token
to be the same as follows.
client.verifyState(token, oauth_token)
Finally, you are ready to generate the access token. Do make sure also to set the oauth_verifier
.
const accessToken = await client.getAccessToken({
token: token,
secre: secret,
}, (request) => {
request.oauth1Param('oauth_verifier', oauth_verifier)
})
The generated access token has the following parameters.
All other response values are merged into the accessToken object, and you can access them directly. For example:
accessToken.scopes
accessToken.idToken
You must create custom drivers for a specific framework. Doing so will help you abstract all the cookie-based state management and input verifications away from the end-user.
Following is the bare minimum setup for a custom driver. Using the lifecycle hooks, you can configure some aspects of the client.
configureRedirectRequest
allows you to configure the redirect request.configureAccessTokenRequest
hook allows you to configure the API request for an access token.processClientResponse
hook let you process the accessToken
response. Here you are given the authorization server's raw response, and you must convert it to an object with at least the following fields.
import { HttpClient } from '@poppinss/oauth-client'
import { Oauth2Client } from '@poppinss/oauth-client/oauth2'
import { Oauth2ClientConfig } from '@poppinss/oauth-client/types'
export class GithubDriver extends Oauth2Client {
constructor(config: Oauth2ClientConfig) {
super(config)
}
/**
* OPTIONAL
*
* Self-process the client response.
*/
protected processClientResponse(client: HttpClient, response: any): any {
/**
* Return JSON as it is when parsed response as JSON
*/
if (client.responseType === 'json') {
return response
}
return parse(client.responseType === 'buffer' ? response.toString() : response)
}
/**
* OPTIONAL
*
* Configure the redirect request. Invoked before
* the user callback
*/
protected configureRedirectRequest(request) {
request.param('scope', 'repo gist user')
}
/**
* OPTIONAL
*
* Configure the access token request. Invoked before
* the user callback
*/
protected configureAccessTokenRequest(request) {
request.param('state', this.ctx.request.cookie('gh_oauth_state'))
}
}
Next, you must implement/overwrite some of the methods to tighten the login experience. For example: Add the redirect
method, which also defines the state
CSRF cookie.
export class GithubDriver extends Oauth2Client {
public redirect() {
const state = this.getState()
myFramework.res.cookie('state', state)
const url = this.getRedirectUrl((request) => {
request.param('state', state)
})
myFramework.res.redirect(url)
}
}
Similarly, you may override the getAccessToken
method and perform the state validation before generating the access token.
export class GithubDriver extends Oauth2Client {
public async getAccessToken(callback?: (request: ApiRequestContract) => void): Promise<Token> {
const existingState = myFramework.req.cookies.state
const inputState = myFramework.req.query.inputState
this.verifyState(existingState, inputState)
return super.getAccessToken(callback)
}
}
You must create custom drivers for a specific framework. Doing so will help you abstract all the cookie-based state management and input verifications away from the end-user.
Following is the bare minimum setup for a custom driver. Using the lifecycle hooks, you can configure some aspects of the client.
The configureRequestTokenRequest
allows you to configure the API request for generating
The configureRedirectRequest
allows you to configure the redirect request.
The configureAccessTokenRequest
hook allows you to configure the API request for the access token.
The processClientResponse
hook let you process the accessToken
response. Here you are given the authorization server's raw response, and you must convert it to an object with at least the following fields.
The hook is called for both the requestToken
and the accessToken
API calls.
import { HttpClient } from '@poppinss/oauth-client'
import { Oauth1Client } from '@poppinss/oauth-client/oauth1'
import { Oauth1ClientConfig } from '@poppinss/oauth-client/types'
export class TwitterDriver extends Oauth1Client {
constructor(config: Oauth1ClientConfig) {
super(config)
}
/**
* Self-process the client response.
*/
protected processClientResponse(
event: 'requestToken' | 'accessToken',
client: HttpClient,
response: any
): any {
/**
* Return JSON as it is when parsed response as JSON
*/
if (client.responseType === 'json') {
return response
}
return parse(client.responseType === 'buffer' ? response.toString() : response)
}
/**
* OPTIONAL
*
* Configure the redirect request. Invoked before
* the user callback
*/
protected configureRedirectRequest(request) {}
/**
* OPTIONAL
*
* Configure the access token request. Invoked before
* the user callback
*/
protected configureAccessTokenRequest(request) {
}
}
Next, you must implement/overwrite some of the methods to tighten the login experience. For example: Add the redirect
method, which also defines the state
CSRF cookie.
export class TwitterDriver extends Oauth1Client {
public async redirect() {
const { token, secret } = await this.getRequestToken()
myFramework.res.cookie('token', token)
myFramework.res.cookie('secret', secret)
const url = this.getRedirectUrl((request) => {
request.param('oauth_token', token)
})
myFramework.res.redirect(url)
}
}
Similarly, you can override the getAccessToken
method and perform the state validation before generating the access token.
export class TwitterDriver extends Oauth1Client {
public async getAccessToken(callback?: (request: ApiRequestContract) => void): Promise<Token> {
const existingToken = myFramework.req.cookies.token
const oauthToken = myFramework.req.query.oauth_token
const existingSecret = myFramework.req.cookies.secret
const oauthVerifier = myFramework.req.query.oauth_verifier
this.verifyState(existingToken, oauthToken)
/**
* Create a child instance, pass it the config with
* two extra params this time and generate the
* access token
*/
return super.getAccessToken({
token: existingToken,
secret: existingSecret
}, callback)
}
}
Following is the list of known exceptions raised by this package.
MissingTokenException
- The exception is raised when:
StateMisMatchException
- The state mismatch exception is raised, when the state cookie does not match the state query param received post redirect.Both the Oauth2Client
and Oauth1Client
class defines the default params or form fields for different API requests. The defined values are usually applicable across Oauth providers. However, you can clear the defaults and define them manually yourself. For example:
The Oauth2Client
defines the following form fields when generating the access token.
request.field('grant_type', 'authorization_code')
request.field('redirect_uri', this.options.callbackUrl)
request.field('client_id', this.options.clientId)
request.field('client_secret', this.options.clientSecret)
You can remove one or all fields as follows.
client.getAccessToken((request) => {
// Clear everthing
request.clear()
// Clear a given field
request.clearField('redirect_uri')
request.field('callback_url', client.options.callbackUrl)
})
The process remains the same for other values as well.
request.clearParam
clears the query string paramrequest.clearOauth1Param
clears the Oauth1 paramrequest.clearHeader
clears the existing headerrequest.clearField
clears the form fieldOauth2Client.getRedirectUrl
The following query params are defined.
client.getRedirectUrl((request) => {
request.clearParam('redirect_uri')
request.clearParam('client_id')
})
Oauth2Client.getAccessToken
The following form fields are defined.
'authorization_code'
client.getAccessToken((request) => {
request.clearField('grant_type')
request.clearField('redirect_uri')
request.clearField('client_id')
request.clearField('client_secret')
})
Oauth1Client.getRequestToken
The following oauth1Params are defined.
client.getRequestToken((request) => {
request.clearOauth1Param('oauth_callback')
})
The Oauth1 specification has two types of parameters. One is added to the URL as a query string, and the other one is added to the Authorization
header, sometimes called the base string.
request.param
method to define the query string param.request.oauth1Param
method to define the Authorization header param.Nothing. It relies on ExpressJS. This package is framework agnostic and ships with the protocol implementation
Not right now 😬. The server-side implementations mainly use the Authorization Code Grant
flow, and I want to keep this package focused on that only.
FAQs
A framework agnostic package to implement "Login with" flow using OAuth compliant authorization servers.
We found that @poppinss/oauth-client demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.
Security News
A Stanford study reveals 9.5% of engineers contribute almost nothing, costing tech $90B annually, with remote work fueling the rise of "ghost engineers."
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.