travisghansen / external-auth-server

easy auth for reverse proxies

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Traefik + EAS + KeyCloak

CanisHelix opened this issue · comments

I am trying to setup EAS to front some lan services.

I created a config-token.json file with the following contents:

        {
          type: "oidc",
          issuer: {
              discover_url: "https://identity.mydomain.com/realms/test/.well-known/openid-configuration",
          },
          client: {
              client_id: "eas",
              client_secret: "supersecretkey"
          },
          scopes: ["openid", "email", "profile"],
          pkce: {
            enabled: false,
            code_challenge_method: "S256"
          },
          nonce: {
            enabled: false,
            ttl: 600
          },
          custom_authorization_parameters: {},
          custom_authorization_code_parameters: {},
          custom_refresh_parameters: {},
          custom_revoke_parameters: {},
          redirect_uri: "https://eas.mydomain.com/oauth/callback",
          features: {
              cookie_expiry: false,
              userinfo_expiry: true,
              session_expiry: true,
              session_expiry_refresh_window: 86400,
              session_retain_id: true,
              refresh_access_token: true,
              fetch_userinfo: true,
              introspect_access_token: false,
              introspect_expiry: 0,
              authorization_token: "access_token",
              filtered_service_headers: [],      
              logout: {
                revoke_tokens_on_logout: [],
                "end_provider_session": {
                  "enabled": false,
                  "post_logout_redirect_uri": "https://eas.mydomain.com/oauth/end-session-redirect"
                },
                "backchannel": {
                  "enabled": false
                },
              },
          },
          assertions: {
              aud: true,
              exp: true,
              nbf: true,
              iss: true,
              sig: {
                enabled: false,
              },
          },
          xhr: {
          },
          csrf_cookie: {
          },
          cookie: {
          },
          custom_error_headers: {},
          custom_service_headers: {},
      }

Using the following command to get a token:

cat config-token.json | docker run --rm -i \
-e EAS_CONFIG_TOKEN_SIGN_SECRET=qvU8f5pDkN \
-e EAS_CONFIG_TOKEN_ENCRYPT_SECRET=ihk4SMaXkd \
-e EAS_ISSUER_SIGN_SECRET=ySNNaneauA \
-e EAS_ISSUER_ENCRYPT_SECRET=3Yu6ntRMiY \
-e EAS_COOKIE_SIGN_SECRET=3xibFU30rn \
-e EAS_COOKIE_ENCRYPT_SECRET=KeHuSHjGEi \
-e EAS_SESSION_ENCRYPT_SECRET=w4lPDX8AC2 \
travisghansen/external-auth-server generate-config-token

In my traefik.toml I have my LAN and Docker Networks set to trustedIP's for forwarding headers. (Prior to this I had missing X-Forward-URI errors)

[entryPoints.web]
  address = ":80"

  [entryPoints.web.http]
    [entryPoints.web.http.redirections]
      [entryPoints.web.http.redirections.entryPoint]
        to = "websecure"
        scheme = "https"

    # Enable Forwarded Headers
    [entryPoints.web.forwardedHeaders]
      trustedIPs = ["10.10.2.0/24","172.16.0.0/12"]

[entryPoints.websecure]
  address = ":443"

  [entryPoints.websecure.http.tls]
    certResolver = "lets-encrypt"

    # Enable Forwarded Headers
    [entryPoints.websecure.forwardedHeaders]
      trustedIPs = ["10.10.2.0/24","172.16.0.0/12"]

In my traefik_dynamic.toml I have the backends for Keycloak/EAS and Firefly setup as follows:

# Hosts
[http.routers]
  # Identity Provider
  [http.routers.identity]
    rule = "Host(`identity.mydomain.com`) && ClientIP(`10.23.2.0/24`,`172.16.0.0/12`)"
    entrypoints = ["websecure"]
    service = "identity"
    [http.routers.identity.tls]
      certResolver = "lets-encrypt"
      [[http.routers.identity.tls.domains]]
        main = "mydomain.com"
        sans = ["*.mydomain.com"]

  [http.routers.eas]
    rule = "Host(`eas.mydomain.com`) && ClientIP(`10.23.2.0/24`,`172.16.0.0/12`)"
    entrypoints = ["websecure"]
    service = "eas"
    [http.routers.eas.tls]
      certResolver = "lets-encrypt"
      [[http.routers.eas.tls.domains]]
        main = "mydomain.com"
        sans = ["*.mydomain.com"]

  [http.routers.firefly]
    rule = "Host(`terra.mydomain.com`) && ClientIP(`10.23.2.0/24`,`172.16.0.0/12`)"
    entrypoints = ["websecure"]
    service = "firefly"
    middlewares = "eas"
    [http.routers.firefly.tls]
      certResolver = "lets-encrypt"
      [[http.routers.firefly.tls.domains]]
        main = "mydomain.com"
        sans = ["*.mydomain.com"]

# Backends
[http.services]

  # Identity Provider
  [http.services.identity.loadBalancer]
    [[http.services.identity.loadBalancer.servers]]
      url = "http://server1.lan:8004"

  [http.services.eas.loadBalancer]
    [[http.services.eas.loadBalancer.servers]]
      url = "http://server1.lan:8181"

  [http.services.firefly.loadBalancer]
    [[http.services.firefly.loadBalancer.servers]]
      url = "http://server1.lan:8003"

# Middlewares
[http.middlewares]
  [http.middlewares.eas.forwardAuth]
    address = "https://eas.mydomain.com/verify?config_token=<trunc>"
    trustForwardHeader = true
    authResponseHeaders = ["X-Auth-Username", "X-Auth-Email", "X-Forwarded-Uri", "X-Forwarded-User", "X-Access-Token", "Authorization"]

Unfortunately navigating to terra.mydomain.com results in a 503, no prompt for Keycloak at all and EAS's docker (with silly log level) reports the following:

error: error:0606506D:digital envelope routines:EVP_DecryptFinal_ex:wrong final block length {"stack":"Error: error:0606506D:digital envelope routines:EVP_DecryptFinal_ex:wrong final block length\n    at Object.decrypt (/home/eas/app/src/utils.js:106:11)\n    at _verifyHandler (/home/eas/app/src/server.js:138:46)\n    at verifyHandler (/home/eas/app/src/server.js:94:18)\n    at Layer.handle [as handle_request] (/home/eas/app/node_modules/express/lib/router/layer.js:95:5)\n    at next (/home/eas/app/node_modules/express/lib/router/route.js:144:13)\n    at next (/home/eas/app/node_modules/express/lib/router/route.js:140:7)\n    at next (/home/eas/app/node_modules/express/lib/router/route.js:140:7)\n    at next (/home/eas/app/node_modules/express/lib/router/route.js:140:7)\n    at next (/home/eas/app/node_modules/express/lib/router/route.js:140:7)\n    at next (/home/eas/app/node_modules/express/lib/router/route.js:140:7)","timestamp":"2023-01-29T14:28:15.969Z"}

My docker-compose.yml is and as you can see the secret's are the same.

  eas:
    container_name: eas
    image: travisghansen/external-auth-server:latest
    environment:
      - EAS_PORT=8181
      - EAS_CONFIG_TOKEN_SIGN_SECRET=qvU8f5pDkN
      - EAS_CONFIG_TOKEN_ENCRYPT_SECRET=ihk4SMaXkd
      - EAS_ISSUER_SIGN_SECRET=ySNNaneauA
      - EAS_ISSUER_ENCRYPT_SECRET=3Yu6ntRMiY
      - EAS_COOKIE_SIGN_SECRET=3xibFU30rn
      - EAS_COOKIE_ENCRYPT_SECRET=KeHuSHjGEi
      - EAS_SESSION_ENCRYPT_SECRET=w4lPDX8AC2
      - LOG_LEVEL=silly
    ports:
      - 8181:8181/tcp
    restart: always

After a day of this I'm stuck as to what could be the problem here. Anything I have done wrong in the above?

Welcome! Sorry it’s been such a struggle :( I’m sure we can get you going though.

So generally the process you followed looks sane. The structure of your json is off a little bit and what you sent seems like invalid json as well. But that may not be your issue currently anyway. Can you send the output/config token data from running the docker run command?

And send over the logs previous to the 503 line/error as well.

While waiting for further details, here are some notes:

  1. the structure of the json is off, use something like the following (you can use yaml vs json):
aud: someaudience
eas:
  plugins:
    - type: oidc
      issuer:
        discover_url: >-
          https://identity.mydomain.com/realms/test/.well-known/openid-configuration
      client:
        client_id: eas
        client_secret: supersecretkey
      scopes:
        - openid
        - email
        - profile
      pkce:
        enabled: false
        code_challenge_method: S256
      nonce:
        enabled: false
        ttl: 600
      custom_authorization_parameters: {}
      custom_authorization_code_parameters: {}
      custom_refresh_parameters: {}
      custom_revoke_parameters: {}
      redirect_uri: 'https://eas.mydomain.com/oauth/callback'
      features:
        cookie_expiry: false
        userinfo_expiry: true
        session_expiry: true
        session_expiry_refresh_window: 86400
        session_retain_id: true
        refresh_access_token: true
        fetch_userinfo: true
        introspect_access_token: false
        introspect_expiry: 0
        authorization_token: access_token
        filtered_service_headers: []
        logout:
          revoke_tokens_on_logout: []
          end_provider_session:
            enabled: false
            post_logout_redirect_uri: 'https://eas.mydomain.com/oauth/end-session-redirect'
          backchannel:
            enabled: false
      assertions:
        aud: true
        exp: true
        nbf: true
        iss: true
        sig:
          enabled: false
      xhr: {}
      csrf_cookie: {}
      cookie: {}
      custom_error_headers: {}
      custom_service_headers: {}
  1. if your keycloak env supports it I would suggest enabling nonce and pkce, they provide the best security
  2. (this is likely your primary issue) the config_token as returned using the command above is not url encoded. Run the token through your encoder of choice or something like https://www.urlencoder.org/ to get the values as it should be in your traefik config (ie: address = "https://eas.mydomain.com/verify?config_token=<THE ENCODED STRING HERE>"
  3. ensure that the eas is not protected by the eas middleware configuration...that will not end well :)

Thanks for the quick feedback.

I ripped the config_token straight from an example in HOWTO.md (https://github.com/travisghansen/external-auth-server/blob/master/HOWTO.md#generage-config_token) but at a late hour I clearly mistook Javascript for JSON. But then I expected the generate-config-token command to detect that and produce an error instead of providing a token back.

I've corrected that now, and used https://www.urlencoder.org/ and now it is working as expected.

Regarding PKCE/Nonce, I'm still new to Keycloak (latest version) and not even sure how to enable these yet on the realm? client? server?

With the advice given I've almost got this working. The end service is expecting the email address in a single header containing only an email address. But X-Auth-Email and X-Auth-Username are empty, X-Userinfo (I added this as a forwarded header to Traefik) is passed along, but the end service needs just the email.

So the only challenge is figuring out how to get just the Email into a header via Traefik/Keycloak/EAS. Still new to SSO so learning lot's here.

Yup! This project is super powerful but also takes a minute to grok all the config. I can send you exact syntax but I'll need you to send over samples of the id_token/userinfo data so I know exactly where in the data the relevant datapoints reside. The mechanism for doing this is the custom_service_headers feature.

This is super powerful, but definitely takes some time to understand the configuration. Just found custom_service_headers and reading some examples.

The X-Userinfo being passed looks something like this:

{"sub":"991fed2d-f5ed-490e-88b5-268100b63ebe","email_verified":true,"name":"FirstName LastName","preferred_username":"firstname","given_name":"FirstName","family_name":"LastName","email":"me@mydomain.com"}

So I'm thinking something like this might work:

     "custom_service_headers": {
        "X-Auth-Email": {
            "source": "userinfo",
            "encoding": "plain",
            "query_engine": "jq",
            "query": "[ .emails[].email ] | first",
        },
        "X-Auth-Username": {
            "source": "userinfo",
            "encoding": "plain",
            "query_engine": "jq",
            "query": "[ .preferred_username ]",
        }
    } 

For best performance use jp query_engine:

Something like this should make it all work:

     "custom_service_headers": {
        "X-Auth-Email": {
            "source": "userinfo",
            "encoding": "plain",
            "query_engine": "jp",
            "query": "$.email",
            "query_opts": {
                "single_value": true
            }
        },
        "X-Auth-Username": {
            "source": "userinfo",
            "encoding": "plain",
            "query_engine": "jp",
            "query": "$.preferred_username",
            "query_opts": {
                "single_value": true
            }
        }
    }

That works perfectly too. Thanks ever so much with the help in getting this up and running.

No problem! Enjoy!

Another possibility is to include email in your scopes and use the data from the id_token instead of userinfo as userinfo requires an api call to the provider to get the extra data. eas can/will cache userinfo data to keep the overhead down but if the needs are very slim it may make sense to go that route.