Как я могу добавить параметры к полезной нагрузке Loopback 4 JWT?

#jwt #loopback4

#jwt #loopback4

Вопрос:

У меня есть loopback 4 проект, и я использую @loopback/authentication: ^6.0.0 для добавления аутентификации JWT. Я следовал официальной документации и подключил ее к своему MongoDB. Все это прошло хорошо, и я могу защитить конечные точки. Однако прогресс резко остановился. Когда пользователь входит в систему, система генерирует токен JWT, и мне нужно добавить userId к полезной нагрузке. Я просто не могу понять, как добавить что-либо к полезной нагрузке.

Код, который создает JWT при входе в систему, является:

   @post('/users/login', {
    responses: {
      '200': {
        description: 'Token',
        content: {
          'application/json': {
            schema: {
              type: 'object',
              properties: {
                token: {
                  type: 'string',
                },
              },
            },
          },
        },
      },
    },
  })
  async login(
    @requestBody(CredentialsRequestBody) credentials: Credentials,
  ): Promise<{token: string}> {
    // ensure the user exists, and the password is correct
    const user = await this.userService.verifyCredentials(credentials);
    // convert a User object into a UserProfile object (reduced set of properties)
    const userProfile = this.userService.convertToUserProfile(user);
    // create a JSON Web Token based on the user profile
    const token = await this.jwtService.generateToken(userProfile);
    return {token};
  }
  

Сначала я попытался вручную добавить userId из объекта user в объект userProfile, но это не сработало. Когда я проверяю generateToken метод, я обнаруживаю, что он введен в UserProfile , который определяется как:

 export interface UserProfile extends Principal {
    email?: string;
    name?: string;
}
  

Просто для проверки я попытался добавить свой userID: number; параметр, но это не работает. Генерируемая полезная нагрузка всегда:

 {
  "id": "84f0106e-3d47-4af5-8ea3-f8d41194be87",
  "name": "chrisloughnane",
  "email": "test@gmail.com",
  "iat": 1597973078,
  "exp": 1597994678
}
  

name Параметр также сбивает с толку, потому что я не указываю, что a name в моем User определении объекта. У него есть username , как он это связывает, я не могу найти код.

Как я могу добавить дополнительные параметры к полезной нагрузке generate JWT?

Любопытно, что когда запрос направляется к защищенной конечной точке, эта полезная нагрузка затем обрабатывается, так что userId используется для извлечения правильных данных. Я могу написать функцию для ручного декодирования полезной нагрузки, есть ли встроенная функция для этого?

ОБНОВЛЕНИЕ: Решение для доступа к полезной нагрузке, которое я придумал, состояло в том, чтобы внедрить SecurityBindings.USER в конструктор моего контроллера, а затем присвоить его переменной, которую можно использовать в любой конечной точке.

 constructor(
    @repository(IconsRepository)
    public iconsRepository: IconsRepository,
    @inject(SecurityBindings.USER, {optional: true})
    public user: UserProfile,
  ) {
    this.userId = this.user[securityId];
  }
  

Ответ №1:

Вам нужно расширить пользовательский интерфейс.

Пример:

 export interface UserJWT extends UserProfile {
   someProperty: string;
}
  

Полный пример следующий:

ШАГ 1: Рефакторинг JWTService

 @bind({scope: BindingScope.TRANSIENT})
export class JWTService implements TokenService {
    constructor() {}

    async verifyToken(token: string): Promise<UserJWT> {
        try {
            return await jwt.verify(token, 'your secret') as UserJWT;
        } catch (err) {
            throw new HttpErrors.Unauthorized('Invalid token');
        }
    }

    async generateToken(user: UserJWT): Promise<string> {
        return await jwt.sign(user, 'secret', {
            expiresIn: 'exp timer'
        });
    }
}
  

ШАГ 2: Привязать службу к keys.ts

 export namespace TokenServiceConstants {
    export const TOKEN_SECRET = 'somevalue';
    export const TOKEN_EXP = '600000';
}

export namespace TokenServiceBindings {
    export const TOKEN_SECRET = BindingKey.create<string> ('authentication.jwt.secret');
    export const TOKEN_EXP = BindingKey.create<string> ('authentication.jwt.expires.in');

    export const TOKEN_SERVICE = BindingKey.create<TokenService> ('services.authentication.jwt.tokenservice'); // TokenService imported from @loopback/authentication !!
}
  

ШАГ 3: Вы могли бы выполнить привязку к новому JWTAuthenticationComponet следующим образом

 export class JWTAuthenticationComponent implements Component {
    bindings: Binding[] = [
        Binding.bind(TokenServiceBindings.TOKEN_SECRET).to(TokenServiceConstants.TOKEN_SECRET),
        Binding.bind(TokenServiceBindings.TOKEN_EXP).to(TokenServiceConstants.TOKEN_EXP),
        Binding.bind(TokenServiceBindings.TOKEN_SERVICE).toClass(JWTService),
        Binding.bind(UserServiceBindings.USER_SERVICE).toClass(MyUserService)
    ]

    constructor(
        @inject(CoreBindings.APPLICATION_INSTANCE) app: Application
    ) {
        registerAuthenticationStrategy(app, JWTAuthenticationStrategy);
    }
}
  

ШАГ 4: Добавьте компонент в application.ts

 this.component(AuthenticationComponent);
this.component(JWTAuthenticationComponent); // imported from previous step
  

ШАГ 5: Добавьте AuthenticationFn в sequence.ts

 constructor(
    @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
    @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
    @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
    @inject(SequenceActions.SEND) public send: Send,
    @inject(SequenceActions.REJECT) public reject: Reject,
    @inject(AuthenticationBindings.AUTH_ACTION) protected authRequest: AuthenticateFn // imported from @loopback/authentication
  ) {}

  async handle(context: RequestContext) {
    try {
      const {request, response} = context;
      const finished = await this.invokeMiddleware(context);
      if (finished) return;
      const route = this.findRoute(request);

      await this.authRequest(request);

      const args = await this.parseParams(request, route);
      const result = await this.invoke(route, args);
      this.send(response, result);
    } catch (err) {
      if (err.code === AUTHENTICATION_STRATEGY_NOT_FOUND || err.code === USER_PROFILE_NOT_FOUND) Object.assign(err, {statusCode: 401});
      
      this.reject(context, err);
    }
  }
  

ШАГ 6: Используйте @authenticate(‘jwt)

 @post('/route', {})
@authenticate('jwt')
async someFunc(): Promise<AnyObject> {}
  

Дополнительно: повторный поиск идентификатора

 @post('/route', {})
@authenticate('jwt')
async someFunc(
    @inject(SecurityBindings.USER) currentUser: UserJWT
): Promise<AnyObject> {
    return { userId: currentUser.id } // NEVER user securityId !!
}
  

Дополнение 2: Используйте generateToken

 constructor(
   @inject(TokenServiceBindings.TOKEN_SERVICE) public tokenService: TokenService
) {}

@post('/route', {})
@authenticate('jwt')
async someFunc(): Promise<AnyObject> {
   return { token: this.tokenService.generateToken(userProfile) }
}
  

Надеюсь, я смогу вам помочь.

Комментарии:

1. спасибо lorenzoli за подробный ответ, я проработаю его в ближайшие день или два.