loopbackio / loopback-next

LoopBack makes it easy to build modern API applications that require complex integrations.
https://loopback.io
Other
4.96k stars 1.07k forks source link

active directory authentication #2263

Closed rajkaran closed 5 years ago

rajkaran commented 5 years ago

I have worked on lb3 before. I have a new application to built which I want to develop in lb4. Application requires LDAP authentication for which I haven't found any documentation yet.

So far all I have got is #671 .

Can anyone guide me how to achieve this or point me to documentation/tutorial.

jannyHou commented 5 years ago

@rajkaran Welcome to LoopBack 4, we don't have a complete document ready for authentication and the team is currently working on building a extensible authentication system that allow users to contribute their auth strategies as extensions.

There is a PR for adding the JWT stragety, see https://github.com/strongloop/loopback4-example-shopping/pull/26, with the JWT strategy written by us. And we are switching to leverage the passport-jwt.

After that we'll come up with a tutorial of how community could contribute their component or extension.

If you interested in adding the LDAP authentication strategy extension, I can help :)

rajkaran commented 5 years ago

I am afraid I won't be able to give much time to it.

ngnam commented 4 years ago

hi @jannyHou :) Can you help me adding the LDAP authentication? Thank YOU

rajkaran commented 3 years ago

I have managed to implement LDAP. It isn't a perfect solution but it works well.

I am using passport-ldapauth strategy. I didn't follow loopback 4 tutorial as ldapauth doesn't have @types or atleast I couldn't find it.

I tried mounting ExpressJs by following Creating an Express Application with LoopBack REST API and then added a route as below

this.app.post('/ldap', function (_req: Request, res: Response, next: any) {
    passport.authenticate('ldapauth', {session: false}, function(err, user, info) {
        if (err) {
          next(err); // will generate a 500 error
        }

        if (! user) {
          return res.status(401).send('Invalid Credentials!')
        }

        return res.send({ success : true, message : 'authentication succeeded' });
    })(_req, res, next);
});

This didn't work out for me as I couldn't figure out how to call UserService or UserRepository in it.

I also tried Mounting an Express Router but, got stuck with same issue as before. Loopback documentation do talks about Using Express middleware but again documentation is not clear enough.

I have built my solution on top of todo-jwt example. Below is the source code of my solution.

src/controllers/user.controller.ts

@post('/users/ldap-login')
  @response(200, {
    description: 'produce token',
    content: {'application/json': {schema: {type: 'object'} } },
  })
  async ldapLogin(
    @inject(RestBindings.Http.REQUEST) req: Request,
    @inject(RestBindings.Http.RESPONSE) res: Response,
  ): Promise<{token: string}> {
    const ldapUser: LdapUserType = await <any>this.ldapHandlerService.authenticateWithLDAP(req, res);
    const user: UserType = await this.whUserService.mapUserFieldWithLdapAttributes(ldapUser)
    const dbUser: User = await this.whUserService.findUserIdentity(user)

    // convert a User object into a UserProfile object (reduced set of properties)
    const userProfile = this.whUserService.convertToUserProfile(dbUser);

    // create a JSON Web Token based on the user profile
    const token = await this.jwtService.generateToken(userProfile);

    return {token};
  }

src/services/wh-user.service.ts

async mapUserFieldWithLdapAttributes(ldapUser: LdapUserType){
    const user: UserType = {givenName: '', familyName: '', username: '', email: '', groups: []};
    const keyValue: {[key: string]: string} =  provider.ldap.profileAttributesFromLDAP;

    for(const property in keyValue){
      const ldapValue = ldapUser[keyValue[property] as keyof typeof ldapUser];

      if(property === 'groups'){
        user['groups'] = Array.isArray(ldapValue)? ldapValue: [ldapValue];
      }
      else{
        user[property as keyof typeof user] = <any>ldapValue;
      }
    }

    return user;
  }

  async findUserIdentity(user: UserType) {
    let foundUser: User | null = await this.userRepository.findOne({ where: {username: user.username} });

    if (foundUser != null) {
      await this.userRepository.updateById(foundUser.id, user)
      foundUser = await this.userRepository.findById(foundUser.id);
    }
    else{
      foundUser = await this.userRepository.create(user);
    }

    return foundUser;
  }

src/services/ldap-handler.service.ts

async authenticateWithLDAP(request: Request, response: Response){
    passport.use(new LdapStrategy({server: provider.ldap.server}));

    return new Promise( (resolve, reject) => {
      passport.authenticate('ldapauth', {session: false}, function(err: string, user: LdapUserType) {
        if (err) reject(new Error(err))
        else if (!user) reject(new Error('Not authenticated'))
        resolve(user);
      })(request, response);
    });
  }

src/provider.json file is same as it used to be in loopback 3

{
  "ldap": {
    "provider": "ldap",
    "authScheme":"ldap",
    "module": "passport-ldapauth",
    "authPath": "/auth/ldap",
    "successRedirect": "/auth/account",
    "failureRedirect": "/ldap",
    "failureFlash": true,
    "session": false,
    "json":true,
    "profileAttributesFromLDAP": {
        "givenName": "",
        "familyName": "",
        "username": "",
        "email": "",
        "groups": ""
    },
    "server":{
        "url": "",
        "bindDn": "",
        "bindCredentials": "",
        "searchBase": "",
        "searchAttributes": [],
        "searchFilter": ""
    }
  }
}

I am providing my solution for those who are still looking for it or if somebody has a better solution please share.

amineba1 commented 2 years ago

can you please give to us your full code ?

rajkaran commented 2 years ago

can you please give to us your full code ?

I do not have access to full source code. Above snippets should be enough to get connected to Active Directory. You will have to make sure provider.json has correct entries.

You can build a pure NodeJS application using same Passport library and then use above code to implement within Loopback 4. That's how I figured it out.