Keycloak Invalid token issuer

239 阅读5分钟

Keycloak 部署在 TKE 上,之前为了在服务中获取客户端的真实 IP,启用了 Ingress Nginx 的 proxy-protocol 1

但是随后发现了一个奇怪的问题,部署在 TKE 集群中的服务,在通过公网域名访问其自身的其它服务提供的接口时不通。

调查过程

通过 ApiFox 接口是可以正常访问的,于是使用 curl 命令在集群中测试了一下。

root@app-liujiajia-deploy-7c66c786b4-gfwp4:/service# curl --location -v --trace trace_connect_sso.txt --request POST 'https://sso.liujiajia.me/auth/realms/testrealm/protocol/openid-connect/token' \
> --header 'Connection: close' \
> --header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
> --header 'Accept: */*' \
> --header 'Host: sso.liujiajia.me' \
> --header 'Content-Type: application/x-www-form-urlencoded' \
> --data-urlencode 'client_id=testclient' \
> --data-urlencode 'grant_type=password' \
> --data-urlencode 'username=user001' \
> --data-urlencode 'password=pwd001'
Warning: --trace overrides an earlier trace/verbose option
Note: Unnecessary use of -X or --request, POST is already inferred.
curl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to sso.liujiajia.me:443 
root@app-liujiajia-deploy-7c66c786b4-gfwp4:/service# cat trace_connect_sso.txt 
== Info:   Trying 159.72.1.1...
== Info: TCP_NODELAY set
== Info: Connected to sso.liujiajia.me (159.72.1.1) port 443 (#0)
== Info: ALPN, offering h2
== Info: ALPN, offering http/1.1
== Info: successfully set certificate verify locations:
== Info:   CAfile: none
  CApath: /etc/ssl/certs
=> Send SSL data, 5 bytes (0x5)
0000: 16 03 01 02 00                                  .....
== Info: TLSv1.3 (OUT), TLS handshake, Client hello (1):
=> Send SSL data, 512 bytes (0x200)
0000: 01 00 01 fc 03 03 77 eb 23 e1 43 b3 0d 51 3a 7b ......w.#.C..Q:{
0010: 68 5d bc c1 20 2e a5 6d 83 54 d4 f5 ad 57 59 94 h].. ..m.T...WY.
0020: a6 33 59 8b 9f 53 20 bb cf 0a 31 06 f7 39 97 ee .3Y..S ...1..9..
......
01e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
== Info: OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to sso.liujiajia.me:443 
== Info: Closing connection 0

请求在建立连接时就失败了,并且提示了 SSL_ERROR_SYSCALL只有第一次发送握手的日志,后续就没了。同样的又测试访问了集群外服务的接口,都可以正常访问。

这个问题一直没解决,调查了很久也没找到原因。唯一能确定的就是关闭 proxy-protocol 后请求可以正常访问。

对于大部分服务来说,调用内部服务时本就应该使用集群内部服务地址,以避免产生公网流量。这部分服务只要调整下配置就可以了。

对于 Keycloak 服务来说,授权时由于是客户端从外网发起的,所以也没有问题,但是在网关中对授权得到的 access_token 进行校验时,改为使用集群内服务地址后,就会出现文章标题的这个 Invalid token issuer 错误。

HTTP/1.1 401 
Date: Tue, 25 Feb 2025 02:19:43 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Connection: close
WWW-Authenticate: Bearer realm="testrealm", error="invalid_token", error_description="Invalid token issuer. Expected 'http://svc-keycloak:8080/auth/realms/testrealm', but was 'https://sso.liujiajia.me/auth/realms/testrealm'"
Strict-Transport-Security: max-age=31536000; includeSubDomains

{
  "code": -1,
  "message": "access_token invalid",
  "data": null,
  "error": "Unauthorized /app/v1/user/info",
}

网关使用的是 keycloak-spring-boot-starter 12.0.1 版本,这个错误是在 RealmUrlCheck 类中抛出的。

@Override
public boolean test(JsonWebToken t) throws VerificationException {
    if (this.realmUrl == null) {
        throw new VerificationException("Realm URL not set");
    }

    if (! this.realmUrl.equals(t.getIssuer())) {
        throw new VerificationException("Invalid token issuer. Expected '" + this.realmUrl + "', but was '" + t.getIssuer() + "'");
    }

    return true;
}

研究了半天源码,也没找到关闭这个校验的开关或方案,不过找到了这个值是通过 ${authServerBaseUrl}/auth/realms/open/.well-known/openid-configuration 端点获取的。其响应中的 issuer 就是上面代码中的 realmUrl,而 issuer 值中的基础地址(即前缀部分)默认和是 authServerBaseUrl 一致的。

authServerBaseUrl 设置为集群内服务地址 http://svc-keycloak:8080/auth 后,realmUrl 会变成 http://svc-keycloak:8080/auth/realms/testrealm

access_token 由于是通过访问 https://sso.liujiajia.me/auth/realms/testrealm/protocol/openid-connect/token 获取的,其 issuer 值为 https://sso.liujiajia.me/auth/realms/testrealm

两者不一致,导致 RealmUrlCheck 校验失败。

代码中没有找到解决办法,就只能到 Keycloak 配置中去找了。在 Realm Settings 页面的 General 选项卡中,有一个 Endpoints 只读项。我这里有两个

  1. OpenID Endpoint Configuration
  2. SAML 2.0 Identity Provider Metadata

其中第一个就是之前发现的获取 issuer 时调用的端点,点开可以看到其 URL 就是 https://sso.liujiajia.me/auth/realms/testrealm/.well-known/openid-configuration

仔细查看了这个页面的各个配置项,发现可以通过修改 Frontend URL 配置项来修改这个端点返回的各种地址中的基础地址,其中就包括 issuer 的值。

[!TIP] Frontend URL

Set the frontend URL for the realm. Use in combination with the default hostname provider to override the base URL for frontend requests for a specific realm.

为领域设置前端 URL。与默认主机名提供者结合使用,以覆盖特定领域前端请求的基 URL。

修改 Frontend URL 为 https://sso.liujiajia.me/auth 后,在网关中即使通过集群内服务地址访问,获取到的 realmUrl 也是 https://sso.liujiajia.me/auth/realms/testrealm,这样 RealmUrlCheck 校验就可以通过了。

解决方案

  1. 修改 keycloak.auth-server-url 配置为 Keycloak 的集群内部 Service 地址:

    keycloak:
      auth-server-url: http://svc-keycloak:8080/auth
    
  2. 修改 Keycloak 管理控制台中 Realm 的 Frontend URL 配置项为集群外部访问地址:https://sso.liujiajia.me/auth

Footnotes

  1. TKE & Ingress-Nginx Controller 之客户端 IP