auth/ldap/LdapProvider.js

  1. /**
  2. * @module flitter-auth/ldap/LdapProvider
  3. */
  4. const Provider = require('../Provider')
  5. const AsyncLdapConnection = require('./AsyncLdapConnection')
  6. const ldap = require('ldap')
  7. /**
  8. * LDAP authentication provider for flitter-auth.
  9. * @extends {module:flitter-auth/Provider~Provider}
  10. * @type {module:flitter-auth/Provider~Provider}
  11. */
  12. class LdapProvider extends Provider {
  13. constructor(app, config) {
  14. super(app, config)
  15. // Create the LDAP connection
  16. /**
  17. * The LDAP connection string in the format 'ldap://...'.
  18. * @type {string}
  19. */
  20. this.connect_string = `ldap${config.secure ? 's' : ''}://${config.host}${config.port ? ':'+config.port : ''}`
  21. const client = ldap.createClient({url: this.connect_string})
  22. /**
  23. * The async LDAP connection.
  24. * @type {module:flitter-auth/ldap/AsyncLdapConnection~AsyncLdapConnection}
  25. */
  26. this.connection = new AsyncLdapConnection(client)
  27. }
  28. /**
  29. * Get an LDAP connection bound to the configured DN.
  30. * @returns {Promise<ldap/Client>}
  31. */
  32. async ldap(){
  33. return this.connection.bind(this.config.bind_dn, this.config.bind_secret, [])
  34. }
  35. /**
  36. * Build the user search filter string. Replaces all instances of '%u' with uid.
  37. * @param {string} uid - uid to be interpolated
  38. * @returns {string}
  39. */
  40. user_filter(uid){
  41. let user_filter = this.config.user_filter
  42. if ( !user_filter.startsWith('(') ) user_filter = `(${userfilter}`
  43. if ( !user_filter.endsWith(')') ) user_filter = `${userfilter})`
  44. return user_filter.replace(/%u/g, uid)
  45. }
  46. /**
  47. * Get an array of user data records matched by the configured filter from the LDAP server.
  48. * @returns {Promise<Array<Object>>}
  49. */
  50. async get_users(){
  51. const ldap = await this.ldap()
  52. return ldap.search(this.config.user_search_base, this.user_filter('*'), Object.values(this.config.attributes));
  53. }
  54. /**
  55. * Get the user data record for the specified user uid matched by the configured filter from the LDAP server.
  56. * @param {string} uid - the user's username
  57. * @returns {Promise<Object|undefined>} - undefined if no user is found with uid
  58. */
  59. async get_user(uid){
  60. const ldap = await this.ldap()
  61. const matches = await ldap.search(this.config.user_search_base, this.user_filter(uid), Object.values(this.config.attributes))
  62. if ( matches.length > 0 ) return matches[0]
  63. }
  64. /**
  65. * Given the user data record from the LDAP server, either look up or create an instance of this.User.
  66. * Store the raw LDAP data in User.data.ldap (as JSON), and update roles where necessary.
  67. * @param {object} data - the data from the LDAP server
  68. * @returns {Promise<module:flitter-auth/model/User~User>}
  69. */
  70. async get_user_object(data){
  71. const User = this.User
  72. const uid = data[this.config.attributes.uid]
  73. const match = await User.findOne({ uid, provider: this.config.name })
  74. if ( match ){
  75. const json = JSON.parse(match.data)
  76. json.ldap = data
  77. match.data = JSON.stringify(json)
  78. await this.set_user_roles(data, match)
  79. await this.set_user_data(data, match)
  80. return match
  81. }
  82. const user = new User({
  83. uid,
  84. provider: this.config.name,
  85. data: JSON.stringify({
  86. ldap: data,
  87. }),
  88. })
  89. await this.set_user_roles(data, user)
  90. await this.set_user_data(data, user)
  91. return user
  92. }
  93. /**
  94. * Update user data from the LDAP record based on model-attribute to ldap-attribute
  95. * mappings in the config (config key: attributes).
  96. * @param {object} data - the user's LDAP data
  97. * @param {module:flitter-auth/model/User~User} user - the user to be updated
  98. * @returns {Promise<void>}
  99. */
  100. async set_user_data(data, user) {
  101. const exclude_attributes = ['group_membership']
  102. for ( const attr in this.config.attributes ) {
  103. if ( Object.keys(data).includes(this.config.attributes[attr]) && !(exclude_attributes.includes(attr)) ) {
  104. user[attr] = data[this.config.attributes[attr]]
  105. }
  106. }
  107. await user.save()
  108. }
  109. /**
  110. * Update the user's auth roles based on the role/group mappings from config. Uses the configured group_membership attribute.
  111. * @param {object} data - user's data record from the LDAP server
  112. * @param {module:flitter-auth/model/User~User} user - the user to be updated
  113. * @returns {Promise<void>}
  114. */
  115. async set_user_roles(data, user) {
  116. let user_roles
  117. if ( !data[this.config.attributes.group_membership] ) user_roles = []
  118. else if ( Array.isArray(data[this.config.attributes.group_membership]) ) user_roles = data[this.config.attributes.group_membership]
  119. else user_roles = [data[this.config.attributes.group_membership]]
  120. for ( let role in this.config.role_groups ){
  121. if ( user_roles.includes(this.config.role_groups[role]) ) user.promote(role)
  122. else user.demote(role)
  123. }
  124. await user.save()
  125. }
  126. /**
  127. * Convert a uid string to a fully qualified DN based on the configured user search base.
  128. * @param {string} uid
  129. * @returns {string} - fully qualified DN of the user
  130. */
  131. uid_to_dn(uid){
  132. return `uid=${uid},${this.config.user_search_base}`
  133. }
  134. /**
  135. * Register a new user with the specified username and attributes.
  136. * Attributes object should contain a 'password' key, which will be removed and used to set the user's LDAP password.
  137. * @param {string} username - uid of the new user
  138. * @param {object} attrs - additional attributes of the user
  139. * @param {object} [data] - additional data to be stored in the user's JSON
  140. * @returns {Promise<module:flitter-auth/model/User~User>}
  141. */
  142. async register(username, attrs, data) {
  143. const entry = {
  144. uid: username,
  145. }
  146. if ( this.config.registration_merge_attributes ){
  147. Object.keys(this.config.registration_merge_attributes).forEach((attr) => {
  148. if ( typeof this.config.registration_merge_attributes[attr] === 'string' ){
  149. entry[attr] = this.config.registration_merge_attributes[attr].replace(/%u/g, username)
  150. }
  151. else entry[attr] = this.config.registration_merge_attributes[attr]
  152. })
  153. }
  154. await this.connection.add(this.uid_to_dn(username), entry)
  155. await this.connection.password_reset(this.uid_to_dn(username), attrs.password)
  156. const user_data = await this.get_user(username)
  157. const user = await this.get_user_object(user_data)
  158. if ( data ) user.data = JSON.stringify({...JSON.parse(user.data), ...data})
  159. Object.keys(attrs).filter(i => i !== 'password').forEach(key => user[key] = attrs[key])
  160. await user.save()
  161. return user
  162. }
  163. /**
  164. * Ensure that registration form_data is valid. Checks for password and unique username.
  165. * @param {object} form_data
  166. * @returns {Promise<Array<string>>} - array of string errors. If empty array, no errors.
  167. */
  168. async validate_registration(form_data){
  169. const errors = await super.validate_registration(form_data)
  170. const min_pw_length = this.config.min_password_length
  171. if ( !Object.keys(form_data).includes('password') || !form_data.password ){
  172. errors.push('Password field is required.')
  173. }
  174. else if ( form_data.password.length < min_pw_length ){
  175. errors.push(`Password must be at least ${min_pw_length} characters long.`)
  176. }
  177. if ( form_data.username ) {
  178. const user = await this.get_user(form_data.username)
  179. if ( user ) errors.push('User already exists with that username.')
  180. }
  181. return errors
  182. }
  183. /**
  184. * From the form data, get the formatted arguments to be passed into the registration function.
  185. * Should create the username and {password} objects.
  186. * @param {object} form_data
  187. * @returns {Promise<Array<*>>}
  188. */
  189. async get_registration_args(form_data){
  190. return [
  191. form_data.username,
  192. {
  193. password: form_data.password,
  194. }
  195. ]
  196. }
  197. /**
  198. * Attempt to authenticate a user with the provided credentials. If it succeeds, return their User object.
  199. * @param {string} username
  200. * @param {string} password
  201. * @param [args] - not required
  202. * @returns {Promise<boolean|module:flitter-auth/model/User~User>} - false if the auth is unsuccessful, a User instance if it is
  203. */
  204. async login(username, password, args = {}){
  205. const data = await this.get_user(username)
  206. if ( !data ) return false
  207. const success = await this.connection.password_check(data.dn, password)
  208. if ( !success ) return false
  209. let user = await this.get_user_object(data)
  210. if ( user.block_login ) return false
  211. user.last_auth = new Date()
  212. await user.save()
  213. return user
  214. }
  215. /**
  216. * Check the validity of the provided credentials.
  217. * @param {string} user
  218. * @param {string} password
  219. * @returns {Promise<boolean>} - true if the credentials succeed, false otherwise
  220. */
  221. async check_user_auth(user, password){
  222. const data = await this.get_user(user)
  223. const ldap = await this.ldap()
  224. return await ldap.password_check(data.dn, password)
  225. }
  226. /**
  227. * Clean up resources used by this provider. Unbinds all open LDAP connections.
  228. * @param {module:libflitter/app/FlitterApp~FlitterApp} app - the current app
  229. * @returns {Promise<void>}
  230. */
  231. async cleanup(app){
  232. this.connection.connection.unbind()
  233. }
  234. }
  235. module.exports = exports = LdapProvider
JAVASCRIPT
Copied!