huly-selfhost icon indicating copy to clipboard operation
huly-selfhost copied to clipboard

OIDC with Authelia: Internal Server Error: The state is missing or does not have enough characters...

Open castaf opened this issue 1 year ago • 3 comments

I add configuration for OpenID Connect. The Account service start is OK. On the front I have the "Continue with OpenId" button. Account service log:

Logging also into console development true
{"level":"info","message":"####################################################################################################################","timestamp":"2024-12-12T16:58:58.515Z"}
{"level":"info","message":"########################SplitLogger account initialized: 2024-12-12T16:58:58.515Z###########################","timestamp":"2024-12-12T16:58:58.515Z"}
Starting account service with brandings:  {}
server started on port 3000
{"issuer":{"authorization_endpoint":"https://authelia.waadoo.net/api/oidc/authorization","backchannel_logout_session_supported":false,"backchannel_logout_supported":false,"claim_types_supported":["normal"],"claims_parameter_supported":false,"claims_supported":["amr","aud","azp","client_id","exp","iat","iss","jti","rat","sub","auth_time","nonce","email","email_verified","alt_emails","groups","preferred_username","name"],"code_challenge_methods_supported":["S256"],"frontchannel_logout_session_supported":false,"frontchannel_logout_supported":false,"grant_types_supported":["authorization_code","implicit"],"id_token_signing_alg_values_supported":["RS256"],"introspection_endpoint":"https://authelia.waadoo.net/api/oidc/introspection","introspection_endpoint_auth_methods_supported":["client_secret_basic"],"issuer":"https://authelia.waadoo.net","jwks_uri":"https://authelia.waadoo.net/jwks.json","request_object_signing_alg_values_supported":["none","RS256"],"request_parameter_supported":false,"request_uri_parameter_supported":false,"require_pushed_authorization_requests":false,"require_request_uri_registration":false,"response_modes_supported":["form_post","query","fragment"],"response_types_supported":["code","token","id_token","code token","code id_token","token id_token","code token id_token","none"],"revocation_endpoint":"https://authelia.waadoo.net/api/oidc/revocation","revocation_endpoint_auth_methods_supported":["client_secret_basic"],"scopes_supported":["offline_access","openid","profile","groups","email"],"subject_types_supported":["public"],"token_endpoint":"https://authelia.waadoo.net/api/oidc/token","token_endpoint_auth_methods_supported":["client_secret_basic"],"userinfo_endpoint":"https://authelia.waadoo.net/api/oidc/userinfo","userinfo_signing_alg_values_supported":["none","RS256"]},"level":"info","message":"Discovered issuer","timestamp":"2024-12-12T16:58:58.600Z"}
{"level":"info","message":"Created OIDC client","timestamp":"2024-12-12T16:58:58.601Z"}
{"level":"info","message":"Registered OIDC strategy","timestamp":"2024-12-12T16:58:58.602Z"}
{"level":"info","message":"try auth via","provider":"openid","timestamp":"2024-12-12T16:59:53.287Z"}

When I click on the "Continue with OpenId" button, I am well redirected on my Idp Authelia, but I instantly redirect on Huly account service with an HTTP 500 error and an error from Authelia:

level=error msg="Authorization Request failed with error: The state is missing or does not have enough characters and is therefore considered too weak. Request parameter 'state' must be at least be 8 characters long to ensure sufficient entropy."

Indeed, the request GET parameters of the first redirection on Authelia are the following:

scheme
	https
host
	account.huly.waadoo.net
filename
	/auth/openid/callback
error
	invalid_state
error_description
	The state is missing or does not have enough characters and is therefore considered too weak. Request parameter 'state' must be at least be 8 characters long to ensure sufficient entropy.
state
	%7B%7D

Indeed, the state parameter length is less than 8 characters. Also there is not nonce parameter sent.

castaf avatar Dec 13 '24 15:12 castaf

The same issue with gitlab oidc

Environment variables:

OPENID_CLIENT_ID=private
OPENID_CLIENT_SECRET=private
OPENID_ISSUER=https://gitlab.mydomain.com

Error:

Image

Logs:

{"level":"info","message":"try auth via","provider":"openid","timestamp":"2025-02-22T18:16:45.511Z"}

  Error: did not find expected authorization request details in session, req.session["oidc:gitlab.mydomain.com"] is undefined
      at /usr/src/app/bundle.js:210011:17
      at OpenIDConnectStrategy.authenticate (/usr/src/app/bundle.js:210059:9)
      at attempt (/usr/src/app/bundle.js:196557:20)
      at authenticate (/usr/src/app/bundle.js:196558:23)
      at /usr/src/app/bundle.js:197207:11
      at new Promise (<anonymous>)
      at /usr/src/app/bundle.js:197206:16
      at /usr/src/app/bundle.js:197181:11
      at new Promise (<anonymous>)
      at passportAuthenticate (/usr/src/app/bundle.js:197153:19)
      at router.get.email (/usr/src/app/bundle.js:210188:13)
      at dispatch (/usr/src/app/bundle.js:212328:36)
      at /usr/src/app/bundle.js:223081:20
      at dispatch (/usr/src/app/bundle.js:212328:36)
      at /usr/src/app/bundle.js:212320:16
      at dispatch (/usr/src/app/bundle.js:223085:35)

marvincorreia avatar Feb 22 '25 18:02 marvincorreia

> level=error msg="Authorization Request failed with error: The state is missing or does not have enough characters and is therefore considered too weak. Request parameter 'state' must be at least be 8 characters long to ensure sufficient entropy."

I found a solution to this, though it feels like maybe it's a workaround. I'd appreciate any commentary, in case there is another more appropriate path to resolution.

With everything configured for OIDC via Authelia, I received the same error complaining about state being missing or insufficient characters. My OIDC setup with other apps in my homelab were working fine, so I followed the Huly code.

The error stems from the Huly Account service reply to the request at: {HOST}/_accounts/auth/openid

Code https://github.com/hcengineering/platform/blob/develop/pods/authProviders/src/openid.ts#L69 shows that we are creating the state here :

router.get('/auth/openid', async (ctx, next) => {
    measureCtx.info('try auth via', { provider: 'openid' })
    const state = encodeState(ctx, brandings)

    await passport.authenticate('oidc', {
      scope: 'openid profile email',
      state
    })(ctx, next)
  })

Which is defined at https://github.com/hcengineering/platform/blob/develop/pods/authProviders/src/utils.ts#L49 :

export function encodeState (ctx: any, brandings: BrandingMap): string {
  const host = getHost(ctx.request.headers)
  const branding = host !== undefined ? brandings[host]?.key ?? undefined : undefined
  const state: AuthState = {
    inviteId: ctx.query?.inviteId,
    branding,
    autoJoin: ctx.query?.autoJoin !== undefined,
    navigateUrl: ctx.query?.navigateUrl
  }

  return encodeURIComponent(JSON.stringify(state))
}

In this method the AuthState is made up of four things, three of which are retrieved from the query string. In my case, there was no query string supplied. This meant that the entirety of the state was based on the branding, which is supplied to the encodeState function. As we see in the first code snippet, the router simply passes this value on, with it's source being https://github.com/hcengineering/platform/blob/develop/pods/authProviders/src/openid.ts#L31 :

export function registerOpenid (
  measureCtx: MeasureContext,
  passport: Passport,
  router: Router<any, any>,
  accountsUrl: string,
  dbPromise: Promise<AccountDB>,
  frontUrl: string,
  brandings: BrandingMap,
  signUpDisabled?: boolean
)

Following the code looking for the source of the brandings, the registerOpenid function is seen here https://github.com/hcengineering/platform/blob/develop/pods/authProviders/src/index.ts#L67 within the registerProviders function, then https://github.com/hcengineering/platform/blob/develop/pods/authProviders/src/index.ts#L26 called here in the account-service itself https://github.com/hcengineering/platform/blob/develop/server/account-service/src/index.ts#L131 which gets brandings from https://github.com/hcengineering/platform/blob/develop/server/account-service/src/index.ts#L43 method serveAccount export function serveAccount (measureCtx: MeasureContext, brandings: BrandingMap, onClose?: () => void)

And finally here https://github.com/hcengineering/platform/blob/develop/pods/account/src/__start.ts#L30 we see where the brandings comes from, calling a loadBrandingMap(brandingPath) function:

const brandingPath = process.env.BRANDING_PATH

serveAccount(metricsContext, loadBrandingMap(brandingPath), () => {})

First thing to note, the environment var BRANDING_PATH for the accounts service points to the source of the brandings configuration, and it is loaded when the account service starts up here https://github.com/hcengineering/platform/blob/develop/server/core/src/utils.ts#L220 :

export function loadBrandingMap (brandingPath?: string): BrandingMap {
  let brandings: BrandingMap = {}
  if (brandingPath !== undefined && brandingPath !== '') {
    brandings = JSON.parse(fs.readFileSync(brandingPath, 'utf8'))

    for (const [host, value] of Object.entries(brandings)) {
      const protocol = value.protocol ?? 'https'
      value.front = `${protocol}://${host}/`
    }
  }

  return brandings
}

Aaaaaaaand during initial setup of Huly, my docker compose file had no reference to the BRANDING_PATH, which will by default be an empty object.

AHA! So, jumping back to the code where AuthState is defined - because my request had no query string AND there was no configured BRANDING_PATH, the AuthState would always be an empty object and cause the OIDC process to fail with the noted error that the state was missing/too short.

My solve required two things. First, create a branding.json file on my host system. The content of this file is defined by a Branding object https://github.com/hcengineering/platform/blob/develop/packages/core/src/server.ts#L106 and this is what my file looked like (change "mydomain.com" to whatever your Huly host domain is:

{
  "mydomain.com": {
    "key": "mydomain.com",
    "title": "Huly",
    "languages": "en,ru,pt,es,zh,fr,de,ja",
    "defaultLanguage": "en",
    "defaultApplication": "tracker",
    "defaultSpace": "tracker:project:DefaultProject",
    "lastNameFirst": "true",
    "defaultSpecial": "issues",
    "links": [
      {
        "rel": "manifest",
        "href": "/huly/site.webmanifest"
      },
      {
        "rel": "icon",
        "href": "/huly/favicon.svg",
        "type": "image/svg+xml"
      },
      {
        "rel": "shortcut icon",
        "href": "/huly/favicon.ico",
        "sizes": "any"
      },
      {
        "rel": "apple-touch-icon",
        "href": "/huly/apple-touch-icon.png"
      }
    ]
  }
}

Remember from the first code snippet, the branding chosen from the brandings.json requires a key that matches the host domain:

const host = getHost(ctx.request.headers)
const branding = host !== undefined ? brandings[host]?.key ?? undefined : undefined

Second thing was to update my docker compose for the account service to map the branding.json file to the container, and set the environment variable for BRANDING_PATH accordingly:

  account:
    image: hardcoreeng/account:${HULY_VERSION}
    volumes:
      - ./brandings.json:/usr/src/app/brandings.json
    environment:
      - SERVER_PORT=3000
      - SERVER_SECRET=${SECRET}
      - DB_URL=mongodb://mongodb:27017
      - MONGO_URL=mongodb://mongodb:27017
      - TRANSACTOR_URL=ws://transactor:3333;ws${SECURE:+s}://${HOST_ADDRESS}/_transactor
      - STORAGE_CONFIG=minio|minio?accessKey=minioadmin&secretKey=minioadmin
      - FRONT_URL=http://front:8080
      - STATS_URL=http://stats:4900
      - MODEL_ENABLED=*
      - ACCOUNTS_URL=http${SECURE:+s}://${HOST_ADDRESS}/_accounts
      - ACCOUNT_PORT=3000
      - OPENID_CLIENT_ID=uhvz5Yw8XvsWRXOJYVn7sHxo8qbPmMaFfMeYQejewIczTJSyx-sB7DDMeFlLQ2vRtcjfQVGM
      - OPENID_CLIENT_SECRET=Ite7yTOBb_3kf2tzKT8zmf9_mAM-nABc8ai_VDF5QxBFqy0BLs4beZV3ETp.fojn7jz0fTf6
      - OPENID_ISSUER=http${SECURE:+s}://${HOST_AUTH_ADDRESS}
      - BRANDING_PATH=/usr/src/app/brandings.json
    restart: unless-stopped

Restart the Huly account docker container with this in place, and my OIDC login process finally works!

Question to the devs - is there an expectation that the invocation to {HOST}/_accounts/auth/openid is supposed to have query string parameters? Is defining the BRANDING_PATH the way that I did a workaround for something that I should have done differently? Perhaps I missed a step somewhere along the way that would have made this work more out of the box?

ashenatore avatar Jun 04 '25 20:06 ashenatore

Hi @ashenatore, thank you for the thorough and well-documented analysis, it is spot-on. To answer your question - no, you should not need to define the branding or have any query parameters as a workaround. This should work out of the box, and we'll fix this on our end to make it work as expected in this scenario.

lexiv0re avatar Jun 04 '25 20:06 lexiv0re