Last active
May 2, 2024 02:14
-
-
Save nicolasdao/a17f575a65ddad166d51aa7e78e41be7 to your computer and use it in GitHub Desktop.
UserIn strategy template. Keywords: userin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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