keycloak api_使用Keycloak保护NestJS API
keycloak api
From the Keycloak website, “Keycloak is an open source identity and access management solution”. Today we’ll look at how to protect your HTTP API with Keycloak.
在Keycloak网站上,“ Keycloak是一种开源身份和访问管理解决方案”。 今天,我们将研究如何使用Keycloak保护您的HTTP API。
One of the modern ways to protect an HTTP API today is via the “Authorization: Bearer <token>” HTTP header and with the token being a JWT carrying the identity and the claims (roles, etc.) of the consumer of the API.
今天,保护HTTP API的一种现代方法是通过“授权:承载<令牌>” HTTP标头,并且令牌是一个JWT,它带有API使用者的身份和声明(角色等)。
We’ll assume you already have a JS frontend app or at least a HTTP client that performed the authentication against Keycloak and is in possession of a JWT and can pass it as a HTTP “Authorization: Bearer <token>” header to your NestJS backend.
我们假设您已经有一个JS前端应用程序或至少一个对Keycloak执行身份验证并拥有JWT的HTTP客户端,并且可以将其作为HTTP“ Authorization:Bearer <token>”标头传递给NestJS后端。
JWTs can be symmetrically signed (same secret to sign and to verify the JWT) or asymmetrically (token signed with private key and verifiable with the corresponding public key). Keycloak uses the later which is great because it allows multiple backends to be able to verify JWTs without disseminating a secret across multiple services. It means that if one of your service is compromised, at least an attacker won’t be able to forge JWTs on its own to attack other services.
JWT可以对称签名(签名和验证JWT的秘密相同),也可以不对称(使用私钥签名的令牌,并可以使用相应的公钥验证)。 Keycloak使用了后者,这是很好的选择,因为它允许多个后端能够验证JWT,而无需在多个服务之间传播秘密。 这意味着,如果您的一项服务遭到破坏,至少攻击者将无法伪造JWT来攻击其他服务。
实作 (Implementation)
We need to write a Guard that will decorate the controllers or the individual handlers that we want to protect.
我们需要编写一个Guard,它将装饰我们要保护的控制器或单个处理程序。
This guard will use an AuthenticationService which will perform (in various ways as you’ll see below) the verification of the JWT.
该防护将使用AuthenticationService ,它将执行JWT的验证 (以各种方式,如下所示)。
All the required services will be part of an AuthenticationModule that will export some of them that may be required by the rest of your application.
所有必需的服务将成为AuthenticationModule的一部分,该身份验证模块将导出应用程序其余部分可能需要的其中一些服务。
We’ll provide a working implementation and refine it later to make it more practical to use in production, in E2E tests, etc.
我们将提供一个可行的实施方案,并在以后对其进行完善,使其在生产,端到端测试等方面更加实用。
Let’s go:
我们走吧:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthenticationModule } from './authentication/authentication.module';
@Module({
imports: [
AuthenticationModule,
],
controllers: [
AppController,
],
providers: [
AppService,
],
})
export class AppModule {}
If you want the full code of the application at this state. Here is the commit: https://github.com/paztek/nestjs-authentication-example/tree/7376afb2691fdd6972eceb70ceda86e1103b716d
如果要在此状态下应用程序的完整代码。 这是提交: https : //github.com/paztek/nestjs-authentication-example/tree/7376afb2691fdd6972eceb70ceda86e1103b716d
改进之处 (Improvements)
One of the things that we can improve is providing a way to not have a Keycloak running during your E2E tests. Using a real Keycloak instance during your E2E tests, while more realistic, makes your tests slow by requiring additional HTTP calls, waiting for Keycloak to start and preloading it with a list of users with various sets of roles.
我们可以改善的事情之一就是提供一种在端到端测试期间不运行Keycloak的方法。 在端到端测试期间使用真实的Keycloak实例虽然更现实,但由于需要额外的HTTP调用,等待Keycloak启动并向其预加载具有各种角色集的用户列表,因此使测试变慢。
At the moment, one reasonable way to bypass Keycloak in E2E tests is to swap the AuthenticationService with a FakeAuthenticationService. But that will become less and less acceptable as your AuthenticationService encapsulates more and more logic (creating users in your local database, counting sessions or analytics, etc.).
目前,在E2E测试中绕过Keycloak的一种合理方法是将AuthenticationService与FakeAuthenticationService交换。 但是,随着AuthenticationService封装越来越多的逻辑(在本地数据库中创建用户,计算会话或分析等),这将变得越来越不被接受。
Another way is to stub the HTTP calls made with the HttpService but once your HttpService is also used to perform HTTP calls to other services, it will become more and more complicated.
另一种方法是对用HttpService进行的HTTP调用进行存根,但是一旦您的HttpService也用于对其他服务执行HTTP调用,它将变得越来越复杂。
Below, I propose a solution that keeps your tests realistic enough while removing the need for a running Keycloak instance.
下面,我提出一种解决方案, 使您的测试足够现实,同时又不需要运行Keycloak实例 。
import { HttpModule, Module } from '@nestjs/common';
import { AuthenticationGuard } from './authentication.guard';
import { AuthenticationService } from './authentication.service';
import { AUTHENTICATION_STRATEGY_TOKEN } from './authentication.strategy';
import { KeycloakAuthenticationStrategy } from './strategy/keycloak.strategy';
import { FakeAuthenticationStrategy } from './strategy/fake.strategy';
@Module({
imports: [
HttpModule,
],
providers: [
AuthenticationGuard,
AuthenticationService,
{
provide: AUTHENTICATION_STRATEGY_TOKEN,
useClass: process.env.NODE_ENV === 'test' ? FakeAuthenticationStrategy : KeycloakAuthenticationStrategy,
},
],
exports: [
AuthenticationService,
],
})
export class AuthenticationModule {}
By delegating just the validation of the JWT to another service, we can keep our AuthenticationService and use a different strategy to validate the JWT during the tests. Don’t forget to add the NODE_ENV=test environment variables.
通过将JWT的验证仅委派给另一个服务,我们可以保留AuthenticationService并在测试期间使用其他策略来验证JWT。 不要忘记添加NODE_ENV = test环境变量。
If you’re allergic to test-related code in the middle of your actual source code and you gasped at the AuthenticationModule, you can do the following:
如果您对实际源代码中间的与测试相关的代码过敏,并且对AuthenticationModule感到喘不过气,则可以执行以下操作:
import { HttpModule, Module } from '@nestjs/common';
import { AuthenticationGuard } from './authentication.guard';
import { AuthenticationService } from './authentication.service';
import { AUTHENTICATION_STRATEGY_TOKEN } from './authentication.strategy';
import { KeycloakAuthenticationStrategy } from './strategy/keycloak.strategy';
@Module({
imports: [
HttpModule,
],
providers: [
AuthenticationGuard,
AuthenticationService,
{
provide: AUTHENTICATION_STRATEGY_TOKEN,
useClass: KeycloakAuthenticationStrategy, // <-- No more test-related code
},
],
exports: [
AuthenticationService,
],
})
export class AuthenticationModule {}
剩余的问题 (Remaining issues)
Keycloak complains when the issuer of the JWT differs from the URL used to get the user infos. It can happen when your frontend negociates its token against the public URL of Keycloak (e.g. https://your-domain.com/auth) but your backend uses the internal address of your Keycloak instance (e.g. http://10.0.1.23/auth).Keycloak’s team doesn’t consider it a bug: https://issues.redhat.com/browse/KEYCLOAK-5045?_sscc=t so we have to work around it.
当JWT的发行者与用于获取用户信息的URL不同时,Keycloak会抱怨。 当您的前端将其令牌与Keycloak的公共URL(例如https://your-domain.com/auth )进行协商但您的后端使用Keycloak实例的内部地址(例如http://10.0.1.23/ )时,可能会发生这种情况。 auth) 。Keycloak的团队认为它不是一个错误: https ://issues.redhat.com/browse/KEYCLOAK-5045?_sscc = t,因此我们必须解决它。
If you can’t easily implement the described workaround (updating the /etc/hosts of your backend so that your-domain.com resolves to 10.0.1.23) you may find the following solution acceptable if you’re not using the token revocation feature of Keycloak and if your access tokens have a short life (e.g. 5 minutes).
如果您无法轻松实现上述解决方法(更新后端的/ etc / hosts以便your-domain.com解析为10.0.1.23),那么您可以在不使用令牌吊销功能的情况下找到以下可接受的解决方案如果您的访问令牌的寿命很短(例如5分钟),请访问Keycloak。
import { HttpService, Injectable } from '@nestjs/common';
import { map } from 'rxjs/operators';
import * as jwt from 'jsonwebtoken';
import { AuthenticationStrategy, KeycloakUserInfoResponse } from '../authentication.strategy';
export class InvalidTokenPublicKeyId extends Error {
constructor(keyId: string) {
super(`Invalid public key ID ${keyId}`);
}
}
/**
* Format of the keys returned in the JSON response from Keycloak for the list of public keys
*/
interface KeycloakCertsResponse {
keys: KeycloakKey[];
}
interface KeycloakKey {
kid: KeyId;
x5c: PublicKey;
}
type KeyId = string;
type PublicKey = string;
@Injectable()
export class ManualAuthenticationStrategy implements AuthenticationStrategy {
/**
* Keep an in-memory map of the known public keys to avoid calling Keycloak every time
*/
private readonly keysMap: Map<KeyId, PublicKey> = new Map<KeyId, PublicKey>();
private readonly baseURL: string;
private readonly realm: string;
constructor(
private httpService: HttpService,
) {
this.baseURL = process.env.KEYCLOAK_BASE_URL;
this.realm = process.env.KEYCLOAK_REALM;
}
public async authenticate(accessToken: string): Promise<KeycloakUserInfoResponse> {
const token = jwt.decode(accessToken, { complete: true }); // For once, we'd like to have the header and not just the payload
const keyId = token.header.kid;
const publicKey = await this.getPublicKey(keyId);
return jwt.verify(accessToken, publicKey);
}
private async getPublicKey(keyId: KeyId): Promise<PublicKey> {
if (this.keysMap.has(keyId)) {
return this.keysMap.get(keyId);
} else {
const keys = await this.httpService.get<KeycloakCertsResponse>(`${this.baseURL}/realms/${this.realm}/protocol/openid-connect/certs`)
.pipe(map((response) => response.data.keys))
.toPromise();
const key = keys.find((k) => k.kid === keyId);
if (key) {
const publicKey =
`
-----BEGIN CERTIFICATE-----
${key.x5c}
-----END CERTIFICATE-----
`;
this.keysMap.set(keyId, publicKey);
return publicKey;
} else {
// Token is probably so old, Keycloak doesn't even advertise the corresponding public key anymore
throw new InvalidTokenPublicKeyId(keyId);
}
}
}
}
We’re verifying the JWT manually instead of asking Keycloak for it. But in order to do that, we have to possess the public key corresponding to the private key used to sign the token.We can’t hardcode the key on the NestJS app because Keycloak changes the signing key periodically. That means we have to call Keycloak to retrieve them.
我们正在手动验证JWT,而不是询问Keycloak。 但是要做到这一点,我们必须拥有与用于签名令牌的私钥相对应的公钥。由于Keycloak会定期更改签名密钥,因此我们无法在NestJS应用上对密钥进行硬编码。 这意味着我们必须调用Keycloak来检索它们。
结论 (Conclusion)
The suggested refactoring of the initial implementation provides several advantages:
建议的对初始实现的重构具有几个优点:
- it makes E2E tests easier to write and run 它使E2E测试更易于编写和运行
- it allowed us to quickly write a workaround when we encountered a limitation from Keycloak in production. 当我们在生产中遇到Keycloak的限制时,它使我们能够快速编写解决方法。
Next time we’ll build upon this implementation and take a look at authorization, by verifying roles of the authenticated user.
下次,我们将在此实现的基础上,通过验证已验证用户的角色来查看授权。
As always, here is the link to the Github repository: https://github.com/paztek/nestjs-authentication-example
与往常一样,这是Github存储库的链接: https : //github.com/paztek/nestjs-authentication-example
翻译自: https://itnext.io/protecting-your-nestjs-api-with-keycloak-8236e0998233
keycloak api