Skip to content

Instantly share code, notes, and snippets.

@nicolasdao
Last active May 2, 2024 02:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nicolasdao/a17f575a65ddad166d51aa7e78e41be7 to your computer and use it in GitHub Desktop.
Save nicolasdao/a17f575a65ddad166d51aa7e78e41be7 to your computer and use it in GitHub Desktop.
UserIn strategy template. Keywords: userin
/**
* Copyright (c) 2020, Cloudless Consulting Pty Ltd.
* All rights reserved.
*
* To use this template, follow the next steps:
*
* 1. Install 'userin-core': npm i userin-core
* 2. Set the constant variable 'STRATEGY' to your liking.
* 3. Rename the class 'YourStrategy' to your liking.
* 4. Implement all the following functions:
* - generate_token
* - get_end_user
* - get_fip_user
* - get_identity_claims
* - get_client
* - get_token_claims
*
* NOTES: Though the template below uses synchronous methods, UserIn support both synchronous and asynchronous methods.
* For example, instead of writing this:
*
* YourStrategy.prototype.generate_token = (root, { type, claims }) => {
* const result = yourCreateTokenMethod(type, claims)
* return result
* }
*
* You can write:
*
* YourStrategy.prototype.generate_token = async (root, { type, claims }) => {
* const result = await yourCreateTokenMethod(type, claims)
* return result
* }
*/
const { Strategy, error:userInError } = require('userin-core')
const STRATEGY = 'yourstrategy'
class YourStrategy extends Strategy {
/**
* Creates a new UserIn Strategy instance.
*
* @param {[String]} config.modes Valid values: 'openid', 'loginsignup' (default).
* @param {String} config.tokenExpiry.access_token [Required] access_token expiry time in seconds.
* @param {String} config.tokenExpiry.refresh_token refresh_token expiry time in seconds. Default null, which means this token never expires.
* @param {Object} config.openid OIDC config. Only required when 'modes' contains 'openid'.
* @param {String} config.openid.iss [Required] OIDC issuer.
* @param {String} config.openid.tokenExpiry.id_token [Required] OIDC id_token expiry time in seconds.
* @param {String} config.openid.tokenExpiry.code [Required] OIDC code expiry time in seconds.
*
*/
constructor(config) {
super(config)
this.name = STRATEGY
}
}
/**
* Filters the profile fields.
* NOTE: This code is just an example. You are free to define whatever fields
* you need in the profile claim. It is also up to you to decide if the profile
* claim is even relevant to you.
*
* @param {Object} entity Full identity object
* @return {Object} profile
*/
const getProfileClaims = entity => {
entity = entity || {}
return {
given_name:entity.given_name || null,
family_name:entity.family_name || null,
zoneinfo: entity.zoneinfo || null
}
}
/**
* Filters the phone fields.
* NOTE: This code is just an example. You are free to define whatever fields
* you need in the phone claim. It is also up to you to decide if the phone
* claim is even relevant to you.
*
* @param {Object} entity Full identity object
* @return {Object} phone
*/
const getPhoneClaims = entity => {
entity = entity || {}
return {
phone:entity.phone || null,
phone_number_verified: entity.phone_number_verified || false
}
}
/**
* Filters the email fields.
* NOTE: This code is just an example. You are free to define whatever fields
* you need in the email claim. It is also up to you to decide if the email
* claim is even relevant to you.
*
* @param {Object} entity Full identity object
* @return {Object} email
*/
const getEmailClaims = entity => {
entity = entity || {}
return {
email:entity.email || null,
email_verified:entity.email_verified || false
}
}
/**
* Filters the address fields.
* NOTE: This code is just an example. You are free to define whatever fields
* you need in the address claim. It is also up to you to decide if the address
* claim is even relevant to you.
*
* @param {Object} entity Full identity object
* @return {Object} address
*/
const getAddressClaims = entity => {
entity = entity || {}
return {
address:entity.address || null
}
}
/**
* Generates a new token or code.
*
* @param {Object} root Previous handler's response. Occurs when there are multiple handlers defined for the same event.
* @param {String} type Values are restricted to: 'code', 'access_token', 'id_token', 'refresh_token'
* @param {Object} claims
* @param {String} state This optional value is not strictly necessary, but it could help set some context based on your own requirements.
*
* @return {String} token
*/
YourStrategy.prototype.generate_token = (root, { type, claims }) => {
// Note: You do not need to check the 'type' validity. UserIn has already taken care of this.
// It will always be one of those 4 values: 'code', 'access_token', 'id_token', 'refresh_token'
// IMPORTANT: You are still responsible to implement all the type requirements defined at
// https://github.com/nicolasdao/userin#tokens--authorization-code-requirements
const token = yourCreateTokenMethod(type, claims)
return token
}
/**
* Gets the user's ID and its associated client_ids if this user exists (based on username and password).
*
* @param {Object} root Previous handler's response. Occurs when there are multiple handlers defined for the same event.
* @param {String} user.username
* @param {String} user.password
* @param {String} user... More properties
* @param {String} client_id Optional. Might be useful for logging or other custom business logic.
* @param {String} state Optional. Might be useful for logging or other custom business logic.
*
* @return {Object} user This object should always defined the following properties at a mimumum.
* @return {Object} user.id String ot number
* @return {[Object]} user.client_ids
*/
YourStrategy.prototype.get_end_user = (root, { user }) => {
// Note: You do not need to check that 'user' is truthy or that it defines
// both 'user.username' and 'user.password'. UserIn has already taken care of this.
const existingUser = USER_REPOSITORY.find(x => x.email == user.username)
if (!existingUser)
throw new userInError.InvalidClientError(`user ${user.username} not found`)
if (existingUser.password != user.password)
throw new userInError.InvalidClientError('Incorrect username or password')
const client_ids = USER_TO_CLIENT_REPOSITORY.filter(x => x.user_id == existingUser.id).map(x => x.client_id)
return {
id: existingUser.id,
client_ids
}
}
/**
* Gets the user ID and its associated client_ids if this user exists (based on strategy and FIP's user ID).
*
* @param {Object} root Previous handler's response. Occurs when there are multiple handlers defined for the same event.
* @param {String} strategy FIP name (e.g., 'facebook', 'google')
* @param {Object} user.id FIP's user ID. String or number.
* @param {String} user... More properties
* @param {String} client_id Optional. Might be useful for logging or other custom business logic.
* @param {String} state Optional. Might be useful for logging or other custom business logic.
*
* @return {Object} user This object should always defined the following properties at a mimumum.
* @return {Object} user.id String ot number
* @return {[Object]} user.client_ids
*/
YourStrategy.prototype.get_fip_user = (root, { strategy, user }) => {
// Note: You do not need to check that 'user' and 'strategy' are truthy or that
// 'user.id' is defined. UserIn has already taken care of this.
const existingUser = USER_TO_FIP_REPOSITORY.find(x => x.strategy == strategy && x.strategy_user_id == user.id)
if (!existingUser)
throw new userInError.InvalidClientError(`${strategy} user ID ${user.id} not found`)
const client_ids = USER_TO_CLIENT_REPOSITORY.filter(x => x.user_id == existingUser.user_id).map(x => x.client_id)
return {
id: existingUser.user_id,
client_ids
}
}
/**
* Gets the user's identity claims and its associated client_ids based on the 'scopes'.
*
* @param {Object} root Previous handler's response. Occurs when there are multiple handlers defined for the same event.
* @param {String} user_id
* @param {[String]} scopes
* @param {String} client_id Optional. Might be useful for logging or other custom business logic.
* @param {String} state Optional. Might be useful for logging or other custom business logic.
*
* @return {Object} output.claims e.g., { given_name:'Nic', family_name:'Dao' }
* @return {[Object]} output.client_ids
*/
YourStrategy.prototype.get_identity_claims = (root, { user_id, scopes }) => {
// Note: You do not need to check that 'user_id' is truthy.
// UserIn has already taken care of this.
const user = USER_REPOSITORY.find(x => x.id == user_id)
if (!user)
throw new userInError.InvalidClientError(`user_id ${user_id} not found.`)
const client_ids = USER_TO_CLIENT_REPOSITORY.filter(x => x.user_id == user.id).map(x => x.client_id)
if (!scopes || !scopes.filter(s => s != 'openid').length)
return {
claims: getProfileClaims(user),
client_ids
}
else {
return {
claims: {
...(scopes.some(s => s == 'profile') ? getProfileClaims(user) : {}),
...(scopes.some(s => s == 'email') ? getEmailClaims(user) : {}),
...(scopes.some(s => s == 'address') ? getAddressClaims(user) : {}),
...(scopes.some(s => s == 'phone') ? getPhoneClaims(user) : {})
},
client_ids
}
}
}
/**
* Gets the client's audiences and scopes.
*
* @param {Object} root Previous handler's response. Occurs when there are multiple handlers defined for the same event.
* @param {String} client_id
* @param {String} client_secret Optional. If specified, this method should validate the client_secret.
*
* @return {[String]} output.audiences Service account's audiences.
* @return {[String]} output.scopes Service account's scopes.
*/
YourStrategy.prototype.get_client = (root, { client_id, client_secret }) => {
// Note: You do not need to check that 'client_id' is truthy.
// UserIn has already taken care of this.
const serviceAccount = CLIENT_REPOSITORY.find(x => x.client_id == client_id)
if (!serviceAccount)
throw new userInError.InvalidClientError(`Service account ${client_id} not found`)
if (client_secret && serviceAccount.client_secret != client_secret)
throw new userInError.InvalidClientError('Invalid client')
return {
audiences: serviceAccount.audiences || [],
scopes: serviceAccount.scopes || []
}
}
/**
* Gets a code or a token claims
*
* @param {Object} root Previous handler's response. Occurs when there are multiple handlers defined for the same event.
* @param {String} type Values are restricted to: `code`, `access_token`, `id_token`, `refresh_token`
* @param {Object} token
*
* @return {Object} claims This object should always defined the following properties at a mimumum.
* @return {String} claims.iss
* @return {Object} claims.sub String or number
* @return {String} claims.aud
* @return {Number} claims.exp
* @return {Number} claims.iat
* @return {Object} claims.client_id String or number
* @return {String} claims.scope
*/
YourStrategy.prototype.get_token_claims = (root, { type, token }) => {
// Note: You do not need to check the validity of 'type' or check if 'token' is truthy.
// This has already been taken care upstream by UserIn.
// IMPORTANT: You are still responsible to implement all the type requirements defined at
// https://github.com/nicolasdao/userin#tokens--authorization-code-requirements
const claims = yourGetTokenClaimsMethod(type, token)
return claims
}
module.exports = YourStrategy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment