Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] uncaughtException: Cannot read properties of undefined (reading 'match') #169

Open
Romeren opened this issue Mar 9, 2023 · 37 comments

Comments

@Romeren
Copy link

Romeren commented Mar 9, 2023

Heyo!

I have a EAS installation running in a kubernetes cluster in HA-mode.

I have multiple providers/configurations hooked up to a keycloak instance for authenticating users for a SPA.

Direct sign-in using a keycloak user works.
Signing in with a SSO backed by another keycloak instance also works.
But a final SSO backed by Active directory seems to cause the EAS to crash with the following error:

uncaughtException: Cannot read properties of undefined (reading 'match')
TypeError: Cannot read properties of undefined (reading 'match')
    at Object.parse (/home/eas/app/node_modules/uri-js/dist/es5/uri.all.js:874:29)
    at OpenIdConnectPlugin.get_authorization_redirect_uri (/home/eas/app/src/plugin/oauth/index.js:2514:27)
    at handle_auth_callback_request (/home/eas/app/src/plugin/oauth/index.js:1649:45)
    at OpenIdConnectPlugin.verify (/home/eas/app/src/plugin/oauth/index.js:1924:29)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async processPipeline (/home/eas/app/src/server.js:399:13)

I changed the log-level to silly and reproduced the error:

[
  {
    "jsonPayload": {
      "message": "verify request details: {\"url\":\"/verify?config_token_store_id=<id>&config_token_id_query_engine=jq&config_token_id_query=.parentRequestInfo.parsedUri.host+%7C+split%28%22.%22%29%5B0%5D\",\"params\":{},\"query\":{\"config_token_store_id\":\"<id>\",\"config_token_id_query_engine\":\"jq\",\"config_token_id_query\":\".parentRequestInfo.parsedUri.host | split(\\\".\\\")[0]\"},\"http_method\":\"GET\",\"http_version\":\"1.1\",\"headers\":{\"host\":\"<host>.svc.cluster.local\",\"user-agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36\",\"accept\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\",\"accept-encoding\":\"gzip, deflate, br\",\"accept-language\":\"nl-NL,nl;q=0.9,en-US;q=0.8,en;q=0.7\",\"cache-control\":\"max-age=0\",\"cookie\":\"_hjSessionUser_3324444=eyJ***fQ==; _pk_id.4.0605=ba5*****7.1677162909.\",\"sec-ch-ua\":\"\\\"Chromium\\\";v=\\\"110\\\", \\\"Not A(Brand\\\";v=\\\"24\\\", \\\"Google Chrome\\\";v=\\\"110\\\"\",\"sec-ch-ua-mobile\":\"?0\",\"sec-ch-ua-platform\":\"\\\"Windows\\\"\",\"sec-fetch-dest\":\"document\",\"sec-fetch-mode\":\"navigate\",\"sec-fetch-site\":\"cross-site\",\"upgrade-insecure-requests\":\"1\",\"x-forwarded-for\":\"10.0.0.1\",\"x-forwarded-host\":\"<domain>.com\",\"x-forwarded-method\":\"GET\",\"x-forwarded-port\":\"443\",\"x-forwarded-proto\":\"https\",\"x-forwarded-server\":\"traefik-production-869bc86644-vzp7s\",\"x-forwarded-uri\":\"/?__eas_oauth_handler__=authorization_callback&state=43ff******b546&session_state=c1****-****-****-****-*********59c&code=7*******-****-****-****-***********6d.c1*****-****-****-****-*********59c.e*****-****-****-****-**********40b\",\"x-real-ip\":\"10.0.0.1\"},\"body\":{}}",
      "level": "silly",
      "timestamp": "2023-03-09T06:57:16.354Z",
      "service": "external-auth-server"
    }
  },
  {
    "jsonPayload": {
      "message": "starting verify pipeline",
      "service": "external-auth-server",
      "timestamp": "2023-03-09T06:57:16.354Z",
      "level": "info"
    }
  },
  {
    "jsonPayload": {
      "timestamp": "2023-03-09T06:57:16.354Z",
      "message": "verify params: {\"config_token_store_id\":\"<id>\",\"config_token_id_query_engine\":\"jq\",\"config_token_id_query\":\".parentRequestInfo.parsedUri.host | split(\\\".\\\")[0]\"}",
      "level": "silly",
      "service": "external-auth-server"
    }
  },
  {
    "jsonPayload": {
      "message": "server-side config_token_id query info - query: .parentRequestInfo.parsedUri.host | split(\".\")[0], query_engine: jq, data: {\"req\":{\"headers\":{\"host\":\"<host>.svc.cluster.local\",\"user-agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36\",\"accept\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\",\"accept-encoding\":\"gzip, deflate, br\",\"accept-language\":\"nl-NL,nl;q=0.9,en-US;q=0.8,en;q=0.7\",\"cache-control\":\"max-age=0\",\"cookie\":\"_hjSessionUser_3324444=ey******Q==; _pk_id.4.0605=b****477.1677162909.\",\"sec-ch-ua\":\"\\\"Chromium\\\";v=\\\"110\\\", \\\"Not A(Brand\\\";v=\\\"24\\\", \\\"Google Chrome\\\";v=\\\"110\\\"\",\"sec-ch-ua-mobile\":\"?0\",\"sec-ch-ua-platform\":\"\\\"Windows\\\"\",\"sec-fetch-dest\":\"document\",\"sec-fetch-mode\":\"navigate\",\"sec-fetch-site\":\"cross-site\",\"upgrade-insecure-requests\":\"1\",\"x-forwarded-for\":\"10.0.0.1\",\"x-forwarded-host\":\"<domain>.com\",\"x-forwarded-method\":\"GET\",\"x-forwarded-port\":\"443\",\"x-forwarded-proto\":\"https\",\"x-forwarded-server\":\"traefik-production-869bc86644-vzp7s\",\"x-forwarded-uri\":\"/?__eas_oauth_handler__=authorization_callback&state=43****&session_state=c1...&code=7a....c1...4\",\"x-real-ip\":\"10.0.0.1\"},\"cookies\":{\"_hjSessionUser_3324444\":\"ey...Q==\",\"_pk_id.4.0605\":\"ba59...9.\"},\"signedCookies\":{},\"query\":{\"config_token_store_id\":\"dap2\",\"config_token_id_query_engine\":\"jq\",\"config_token_id_query\":\".parentRequestInfo.parsedUri.host | split(\\\".\\\")[0]\"},\"method\":\"GET\",\"httpVersionMajor\":1,\"httpVersionMinor\":1,\"httpVersion\":\"1.1\"},\"parentRequestInfo\":{\"uri\":\"https://<domain>.com/?__eas_oauth_handler__=authorization_callback&state=4...&session_state=c...&code=7...\",\"parsedUri\":{\"scheme\":\"https\",\"host\":\"<domain>.com\",\"path\":\"/\",\"query\":\"__eas_oauth_handler__=authorization_callback&state=4...&session_state=c...&code=...\",\"reference\":\"absolute\"},\"parsedQuery\":{\"__eas_oauth_handler__\":\"authorization_callback\",\"code\":\"7...\",\"session_state\":\"c...\",\"state\":\"4...\"},\"method\":\"GET\"},\"parentReqInfo\":{\"uri\":\"https://<domain>.com/?__eas_oauth_handler__=authorization_callback&state=4...46&session_state=c...c&code=7...0b\",\"parsedUri\":{\"scheme\":\"https\",\"host\":\"<domain>.com\",\"path\":\"/\",\"query\":\"__eas_oauth_handler__=authorization_callback&state=4...6&session_state=ci...&code=7...b\",\"reference\":\"absolute\"},\"parsedQuery\":{\"__eas_oauth_handler__\":\"authorization_callback\",\"code\":\"7...b\",\"session_state\":\"c1...c\",\"state\":\"4...6\"},\"method\":\"GET\"}}",
      "service": "external-auth-server",
      "level": "debug",
      "timestamp": "2023-03-09T06:57:16.355Z"
    }
  },
  {
    "jsonPayload": {
      "level": "info",
      "service": "external-auth-server",
      "timestamp": "2023-03-09T06:57:16.389Z",
      "message": "sever-side token: store=<id>, id=<domain>"
    }
  },
  {
    "jsonPayload": {
      "service": "external-auth-server",
      "level": "debug",
      "timestamp": "2023-03-09T06:57:16.389Z",
      "message": "adapter config: {\"adapter\":\"sql\",\"options\":{\"config\":{\"client\":\"pg\",\"connection\":{\"database\":\"eas_db\",\"host\":\"10.87.64.3\",\"password\":\"2******\",\"user\":\"******\"},\"pool\":{\"idleTimeoutMillis\":60000,\"max\":10,\"min\":0}},\"query\":\"SELECT token AS token FROM config_tokens WHERE tenant = ? AND client_id = '<id>' AND revoked = False\"}}"
    }
  },
  {
    "jsonPayload": {
      "message": "cache key: config_token_adapater:sql:connections:c4ff***********3c",
      "timestamp": "2023-03-09T06:57:16.389Z",
      "service": "external-auth-server",
      "level": "verbose"
    }
  },
  {
    "jsonPayload": {
      "service": "external-auth-server",
      "timestamp": "2023-03-09T06:57:16.390Z",
      "level": "verbose",
      "message": "cached SQL connection"
    }
  },
  {
    "jsonPayload": {
      "timestamp": "2023-03-09T06:57:16.413Z",
      "level": "debug",
      "service": "external-auth-server",
      "message": "server-side config token: e....I"
    }
  },
  {
    "jsonPayload": {
      "level": "debug",
      "timestamp": "2023-03-09T06:57:16.414Z",
      "message": "config token: {\"eas\":{\"plugins\":[{\"type\":\"oidc\",\"issuer\":{\"discover_url\":\"https://auth.<domain>/auth/realms/<id>/.well-known/openid-configuration\"},\"client\":{\"client_id\":\"<id>\",\"client_secret\":\"8****1\"},\"scopes\":[\"openid\",\"email\",\"profile\"],\"features\":{\"cookie_expiry\":true,\"userinfo_expiry\":true,\"session_expiry\":172800,\"session_expiry_refresh_window\":86400,\"session_retain_id\":true,\"refresh_access_token\":true,\"fetch_userinfo\":true,\"introspect_access_token\":false,\"authorization_token\":\"access_token\"},\"assertions\":{\"exp\":true,\"nbf\":true,\"iss\":true,\"userinfo\":[],\"id_token\":[]},\"cookie\":{\"name\":\"_<name>_session\",\"path\":\"/\"},\"xhr\":{\"redirect_http_code\":401,\"use_referer_as_redirect_uri\":true},\"csrf_cookie\":{\"enabled\":false},\"custom_error_headers\":{},\"custom_service_headers\":{\"X-Token\":{\"source\":\"access_token\",\"query_engine\":\"jp\",\"query\":\"$\",\"query_opts\":{\"single_value\":true}}}}]},\"iat\":1673253928,\"audMD5\":\"216********b3\"}",
      "service": "external-auth-server"
    }
  },
  {
    "jsonPayload": {
      "message": "starting verify for plugin: oidc",
      "service": "external-auth-server",
      "timestamp": "2023-03-09T06:57:16.414Z",
      "level": "info"
    }
  },
  {
    "jsonPayload": {
      "timestamp": "2023-03-09T06:57:16.414Z",
      "service": "external-auth-server",
      "level": "verbose",
      "message": "parent request info: {\"uri\":\"https://<domain>.com/?__eas_oauth_handler__=authorization_callback&state=4....46&session_state=c...9c&code=7***0b\",\"parsedUri\":{\"scheme\":\"https\",\"host\":\"<domain>.com\",\"path\":\"/\",\"query\":\"__eas_oauth_handler__=authorization_callback&state=4...6&session_state=c...c&code=7...b\",\"reference\":\"absolute\"},\"parsedQuery\":{\"__eas_oauth_handler__\":\"authorization_callback\",\"code\":\"7...b\",\"session_state\":\"c...c\",\"state\":\"4....6\"},\"method\":\"GET\"}"
    }
  },
  {
    "jsonPayload": {
      "level": "verbose",
      "message": "audMD5: 2....3",
      "service": "external-auth-server",
      "timestamp": "2023-03-09T06:57:16.414Z"
    }
  },
  {
    "jsonPayload": {
      "service": "external-auth-server",
      "level": "verbose",
      "timestamp": "2023-03-09T06:57:16.414Z",
      "message": "cookie name: _dap2_session"
    }
  },
  {
    "jsonPayload": {
      "message": "decocded state pointer: {\"request_uri\":\"https://<domain>.com/\",\"aud\":\"2...3\",\"csrf\":\"c...0\",\"req\":{\"headers\":{}},\"request_is_xhr\":false,\"iat\":167688102}",
      "service": "external-auth-server",
      "level": "verbose",
      "timestamp": "2023-03-09T06:57:16.415Z"
    }
  },
  {
    "jsonPayload": {
      "level": "verbose",
      "message": "retrieving state: state:oauth:undefined",
      "timestamp": "2023-03-09T06:57:16.415Z",
      "service": "external-auth-server"
    }
  },
  {
    "jsonPayload": {
      "timestamp": "2023-03-09T06:57:16.418Z",
      "service": "external-auth-server",
      "message": "retrieved encrypted state content: null",
      "level": "verbose"
    }
  },
  {
    "jsonPayload": {
      "service": "external-auth-server",
      "level": "verbose",
      "timestamp": "2023-03-09T06:57:16.418Z",
      "message": "failed to decrypt state"
    }
  },
  {
    "jsonPayload": {
      "service": "external-auth-server",
      "message": "decoded state: false",
      "level": "verbose",
      "timestamp": "2023-03-09T06:57:16.418Z"
    }
  },
  {
    "jsonPayload": {
      "message": "audMD5: 2....3",
      "service": "external-auth-server",
      "timestamp": "2023-03-09T06:57:16.418Z",
      "level": "verbose"
    }
  },
  {
    "jsonPayload": {
      "timestamp": "2023-03-09T06:57:16.418Z",
      "level": "verbose",
      "service": "external-auth-server",
      "message": "cookie name: _dap2_session"
    }
  },
  {
    "jsonPayload": {
      "message": "begin token fetch with authorization code",
      "service": "external-auth-server",
      "timestamp": "2023-03-09T06:57:16.418Z",
      "level": "verbose"
    }
  },
  {
    "jsonPayload": {
      "stack": "TypeError: Cannot read properties of undefined (reading 'match')\n    at Object.parse (/home/eas/app/node_modules/uri-js/dist/es5/uri.all.js:874:29)\n    at OpenIdConnectPlugin.get_authorization_redirect_uri (/home/eas/app/src/plugin/oauth/index.js:2514:27)\n    at handle_auth_callback_request (/home/eas/app/src/plugin/oauth/index.js:1649:45)\n    at OpenIdConnectPlugin.verify (/home/eas/app/src/plugin/oauth/index.js:1924:29)\n    at runMicrotasks (<anonymous>)\n    at processTicksAndRejections (node:internal/process/task_queues:96:5)\n    at async processPipeline (/home/eas/app/src/server.js:399:13)",
      "timestamp": "2023-03-09T06:57:16.418Z",
      "message": "Cannot read properties of undefined (reading 'match')",
      "level": "error",
      "service": "external-auth-server"
    }
  },
  {
    "jsonPayload": {
      "service": "external-auth-server",
      "timestamp": "2023-03-09T06:57:16.419Z",
      "message": "end verify pipeline with status: 503",
      "level": "info"
    }
  },
]

Surely I'm simply doing something silly here with the SSO configuration that i have overlooked. Though having trouble pin-pointing exactly what is causing it.

In addition, i think a check is missing here:
https://github.com/travisghansen/external-auth-server/blob/master/src/plugin/oauth/index.js#L1919
Either as a check for null/undefined on the decodedStatePointer.state_id property
Or as a check on the resulting statePayload....

More will follow as i dive deeper...
In the meantime any advice?

@travisghansen
Copy link
Owner

This one will be tricky. Indeed I (from another bug) already have code queued up to detect a null state and return an 403 immediately so don’t worry about that one. The question is assuming a relatively quick login process why is the state not there?

@travisghansen
Copy link
Owner

Wait is this occurring after a logout process has occurred?

@Romeren
Copy link
Author

Romeren commented Mar 9, 2023

To the error occur when:

  1. Navigating to domain.
  2. Getting redirected to keycloak for signing in.
  3. Clicking the SSO button as shown in the image
    image
  4. Signing in on the third party
  5. Being redirected back to the original site.
    Here, the EAS blows up, and 503 is returned
    image

@travisghansen
Copy link
Owner

Does it do that same behavior on fresh incognito window? Looking through the code the only scenario I saw where the state data gets stored as not the state_id is during a logout situation (another bug).

@Romeren
Copy link
Author

Romeren commented Mar 9, 2023

I just double checked...

  • Signing in with the username/password works
  • Signing in with first SSO (another keycloak using OIDC) works
  • Signing in with second SSO (active directory SAML) not working....

I'm quite sure that its the setup of the provider, in relation to the config token that is the issue.

Since the everything works, to the point of the redirect back to the original site, where the EAS is blowing up, I think its the config token that must be the issue here

@Romeren
Copy link
Author

Romeren commented Mar 9, 2023

One sec.... ill check if incognito will help :)

@Romeren
Copy link
Author

Romeren commented Mar 9, 2023

I can confirm.... the problem is not persistent.
It is part of an sign-out and back in that seems to only happen sometimes...

Any way to work around it, other then clearing cookies and app-data...?

Or even better a permanent fix?

@travisghansen
Copy link
Owner

Yeah, I'll fix it up and snap a new release.

@Romeren
Copy link
Author

Romeren commented Mar 9, 2023

Cool, thanks.... Need any help with that?

or anything i can do i the meantime?

@travisghansen
Copy link
Owner

Nah, might be a day or 2 though.

@Romeren
Copy link
Author

Romeren commented Mar 9, 2023

Fair enough! :)
I'll be waiting like a kid for Christmas, if nothing else, i'll be ready to test it when you have it ready

Thanks agian for the quick response

@travisghansen
Copy link
Owner

LOL. Can you send your cleansed config token? You're using some pretty advanced stuff (that I don't test often, but also doesn't change often) so I want to make sure I'm covering the use-case.

@Romeren
Copy link
Author

Romeren commented Mar 9, 2023

This here is the template that we have made.... We are using it to programmatically generate config_tokens so we can have it as IAC:

{
        "eas": {
            "plugins": [{
                "type": "oidc",
                "issuer": {
                    "discover_url": f"{discovery_url_base}/auth/realms/{client.realm}/.well-known/openid-configuration"
                },
                "client": {
                    "client_id": client.id,
                    "client_secret": client.secret
                },
                "scopes": ["openid", "email", "profile"],
                "features": {
                    # how to expire the cookie
                    # true = cookies expire will expire with tokens
                    # false = cookies will be 'session' cookies
                    # num seconds = expire after given number of second
                    "cookie_expiry": True,
                    # how frequently to refresh userinfo data
                    # true = refresh with tokens(assuming they expire)
                    # false = never refresh
                    # num seconds = expire after given number of seconds
                    "userinfo_expiry": True,
                    # how long to keep a session (server side) around
                    # true = expire with tokenSet (if applicable)
                    # false = never expire
                    # num seconds = expire after given number of seconds (enables sliding window)
                    #
                    # sessions become a floating window *if*
                    # - tokens are being refreshed
                    # or
                    # - userinfo being refreshed
                    # or
                    # - session_expiry_refresh_window is a positive number
                    # 48 hours
                    "session_expiry": 172800,
                    # window to update the session window based on activity if
                    # nothing else has updated it (ie: refreshing tokens or userinfo)
                    #
                    # should be a positive number less than session_expiry
                    #
                    # For example, if session_expiry is set to 60 seconds and session_expiry_refresh_window value
                    # is set to 20 then activity in the last 20 seconds (40-60) of the window will 'slide' the window
                    # out session_expiry time from whenever the activity occurred
                    # 24 hours
                    "session_expiry_refresh_window": 86400,
                    # will re-use the same id (ie: same cookie) for a particular client if a session has expired
                    "session_retain_id": True,
                    # if the access token is expired and a refresh token is available, refresh
                    "refresh_access_token": True,
                    # fetch userinfo and include as X-Userinfo header to backing service
                    # only helpful if your specific provider has been implemented
                    "fetch_userinfo": True,
                    "introspect_access_token": False,
                    # which token (if any) to send back to the proxy as the Authorization Bearer value
                    # note the proxy must allow the token to be passed to the backend if desired
                    #
                    # possible values are access_token, or refresh_token
                    "authorization_token": "access_token"
                },
                "assertions": {
                    # assert the token(s) has not expired
                    "exp": True,
                    "nbf": True,
                    "iss": True,
                    # custom userinfo assertions
                    "userinfo": [],
                    "id_token": []
                },
                "cookie": {
                    "name": f"_{client.id}_session",  # default is _oeas_oauth_session
                    # domain: "example.com", # defaults to request domain, could do sso with more generic domain
                    "path": "/"
                    # httpOnly: true,
                    # secure: false,
                    # sameSite: lax,
                },
                # xhr detection is determined by the presence of an 'origin' header OR X-Requested-With: XMLHttpRequest
                "xhr": {
                    # defaults to 302 but could be set to anything
                    # if set to 401 the response will include a WWW-Authenticate header with proper realm/scopes
                    "redirect_http_code": 401,
                    # if set to true, the browser will be redirected to the referer
                    "use_referer_as_redirect_uri": True
                },
                "csrf_cookie": {
                    # TODO - this is disabled following CSRF mismatch problems
                    "enabled": False,  # can disable the use of csrf cookies completely
                    # domain: "example.com", # defaults to request domain, could do sso with more generic domain
                    # path: "/",
                    # httpOnly: true,
                    # secure: false,
                    # sameSite: lax,
                },
                #  see for details: https://github.com/travisghansen/external-auth-server/blob/master/HEADERS.md
                "custom_error_headers": {},
                "custom_service_headers": {
                    "X-Token": {
                        "source": "access_token",
                        "query_engine": "jp",
                        "query": "$",
                        "query_opts": {
                            "single_value": True
                        },
                    }
                }
            }],
        },
        "iat": datetime.utcnow()
    }

Will this do.... or do you want me to fill-in some of the template options

@travisghansen
Copy link
Owner

I don't see how you could hit the block of code I have in mind with that config. The only other explanation is that you have different versions of the app running. What image tag are you using for the deployment? I'm wondering if it's been running for a while and you have some containers using an old latest and others using a newer latest.

@Romeren
Copy link
Author

Romeren commented Mar 9, 2023

Helm chart version: external-auth-server-0.1.0
The image: travisghansen/external-auth-server:latest
ImageId: docker.io/travisghansen/external-auth-server@sha256:59afaf6672b380adfbeb22dfb399b0259d70cd7eb8d8f15c12945c19360d0407

It has indeed been running for a long time.... thought the last update we made to the chart was 2023-03-07 10:48:17.165190604 +0000 UTC deployed

That said, it could be that the container image has been cached in the cluster, and not been updated....

Now that i see that we are running using latest .... i think I'm gonna go ahead and pin the version to something specific

@travisghansen
Copy link
Owner

I'm guessing that's the root of the issue.

https://github.com/travisghansen/external-auth-server/blob/master/CHANGELOG.md#0130

bullet 4 use server-side storage of oauth / oidc state data

It's obviously a complex matrix of what tag you've picked, the pull policy, and the potential introduction of new nodes since that version was released 2023-1-22 but I'm relatively confident that is the issue.

@travisghansen
Copy link
Owner

I would redeploy with v0.13.0 and see if the issue persists.

@Romeren
Copy link
Author

Romeren commented Mar 9, 2023

Give me some hours then....
I'm gonna go through a release cycle and test

@Romeren
Copy link
Author

Romeren commented Mar 9, 2023

Okay....
So made a test. Upgraded the image to version: v0.13.0 and things seems to be working as it should, though now i have to see if i can manage to reproduce the issue....
hold tight and ill be back with more

@Romeren
Copy link
Author

Romeren commented Mar 13, 2023

So was going through the logs after the change on Thursday.
I can see that the issue is still present, 503s are still being returned and i see the following in the logs.

TypeError: Cannot read properties of undefined (reading 'match')
    at Object.parse (/home/eas/app/node_modules/uri-js/dist/es5/uri.all.js:874:29)
    at OpenIdConnectPlugin.get_authorization_redirect_uri (/home/eas/app/src/plugin/oauth/index.js:2514:27)
    at handle_auth_callback_request (/home/eas/app/src/plugin/oauth/index.js:1649:45)
    at OpenIdConnectPlugin.verify (/home/eas/app/src/plugin/oauth/index.js:1924:29)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async processPipeline (/home/eas/app/src/server.js:399:13)

@Romeren
Copy link
Author

Romeren commented Mar 13, 2023

Main thing I'm trying to understand now is how i can have a valid decodedStatePointer, that does not contain a state_id here:
https://github.com/travisghansen/external-auth-server/blob/master/src/plugin/oauth/index.js#L1912

Looking at the content of the decodedStatePointer from line https://github.com/travisghansen/external-auth-server/blob/master/src/plugin/oauth/index.js#L1916
It looks like i end up with the statePayload in the in the state query-parameter rather than the statePointerPayload

decocded state pointer:  {
   "request_uri":"https://<<domain>>.com/",
    "aud":   "216de************09b3",
    "csrf":"c7b74**********0da0",
    "req":{"headers":{}},
     "request_is_xhr":  false,
     "iat": 1676881102
}

Though i cannot quite figure out why.

@travisghansen
Copy link
Owner

The only place I’ve seen that it’s possible to get that is if you use the logout features (which your tokens did not have enabled). Either that or a version mismatch from before the server-side state was implemented.

@Romeren
Copy link
Author

Romeren commented Mar 14, 2023

Right. So i have just wiped the sessions in key-cloak, all keys stored in redis and cycled the deployment of the EAS...
Ill get back to you if the issue starts reappearing

@Romeren
Copy link
Author

Romeren commented Mar 14, 2023

So far so good... Nothing in the logs yet regarding the original issue....

However, I am noticing an similar error that is still causing the EAS to crash:
image

Now it seems that the state pointer contains the actual state ID, but that the state no longer exists when it tries to look it up in Redis. Not sure yet if its related to the wiping of Redis, though it did happen an hour after that was done.

I'll give it more time to see if that is persistent or not

@travisghansen
Copy link
Owner

It could be if you wiped redis. But state has a ttl on the redis entry of an hour or something….so if you take forever it won’t work in that scenario…which seems very unlikely unless someone walks away from their computer midway through the auth process.

@travisghansen
Copy link
Owner

Also note state is very ephemeral. As soon as auth comes back to eas we proactively wipe it out regardless of success or failure with the op.

@travisghansen
Copy link
Owner

Is this for a SPA?

@Romeren
Copy link
Author

Romeren commented Mar 14, 2023

It is indeed a SPA,
All API requests has the 'X-Requested-With': 'XMLHttpRequest' header on them, in case you were wondering

@Romeren
Copy link
Author

Romeren commented Mar 14, 2023

It might be that someone simply stepped away from the computer in the middle of the auth flow.... Though that is indeed odd. Though did not expect that to crash the EAS

@travisghansen
Copy link
Owner

Yeah for sure we shouldn't crash, I'm pretty sure I have fix queued up for that already but will make sure it's in the next release no matter.

Do you have specific logic built into the SPA to handle api failures and redirect? In the case of a SPA there may be some strange behavior due to the concurrent nature of api requests etc but I'd have to think it through more thoroughly to say for sure :(

@Romeren
Copy link
Author

Romeren commented Mar 14, 2023

No worries... If the only remaining error is the person who decided it was time to go for a walk in the middle of the auth flow, then its very far down my priority list.

So for now ill just keep on monitoring and let you know if i see anything else.

For the SPA, we set the X-Requested-With header causing 401s to be returned whenever the session has expired.
We then check the status code on all api requests and do a window.location.reload(true) whenever we get a 401.
This causes the page to refresh and the user then enters the normal auth flow, eg. the session token will be refreshed or the user is asked to sign in again.

This setup actually seems to be working quite nicely, especially after fine-tuning the session_expiry_refresh_window and session_expiry parameters on both keycloak and EAS sides.

In case you wanna get deep into the code;
All API calls basically goes through this piece of code:

import axios from 'axios'
import Vue from 'vue'

const DEFAULT_RETRIES = 1

class HTTPClient {
    constructor(baseURL, retries = DEFAULT_RETRIES) {
        this._retries = retries
        this._axios = axios.create({
            baseURL: baseURL,
            headers: {
                'X-Requested-With': 'XMLHttpRequest'
            }
        })
    }

    async _request(method, url, { data = null, params = null } = {}, retry = 0) {
        if (retry > 0) {
            console.warn(`Retry ${retry} for ${url}`)
        }

        try {
            const response = await this._axios({
                method,
                url,
                data,
                params
            })
            return response.data
        } catch ({ name, message, response }) {
            if (retry < this._retries) {
                return this._request(method, url, { data, params }, retry + 1)
            } else {
                this.errorCheck(response)
                throw new Error(`API call failed for '${url}', with ${name}: ${message}`)
            }
        }
    }

    async get(path, params = null) {
        return await this._request('GET', path, { params })
    }

    async post(path, data) {
        return await this._request('POST', path, { data })
    }
    ....
    errorCheck(response) {
        switch (response.status) {
            case 401:
                window.location.reload(true)
                break
            default:
                return
        }
    }
}

@travisghansen
Copy link
Owner

So both the SPA itself and the API are secured by eas?

@Romeren
Copy link
Author

Romeren commented Mar 15, 2023

Yes they are.

Though i can confirm that that the issue is still present, and that one user didn't even get passed the auth-flow before getting a 503, and that it does not seem to be a side effect of logging out.

Still blowing up on the redirect call back to set the session cookie. The state pointer still has the state payload in its request at this point.

I decrypted the incoming requests query parameter state and can confirm that the issue must be in a earlier step as the state does indeed contain the state payload at this point.

@Romeren
Copy link
Author

Romeren commented Mar 15, 2023

Was unable to reproduce it but since the behavior seem to follow a specific user Ithought it had something to do with the identity provider.

So i created a brand new user in keycloak, singed in and tested that it worked.
Signed out again, sat a temporary password and share it with the affected user.

The user signed in, got asked to change the password. and immediately after finishing up the auth flow got a 503 back and is stuck in the same situation.

I resat the password of the user and tested the user again, and surprise surprise the account and system works again.

So what ever the problem is, its directly linked to this specific users machine.

So ill start gathering information of the specifics here (might be a strange browser or extension).

Also the flow has been tested and is working with Firefox, Chrome, Safari, Opera and Edge

.......

Managed to get on a call with the user.
Now the user cannot reproduce the error, neither with the new account or the SSO account. So problem solved ~ i guess....

I had 10-15 people test it as well, from every conceivable OS platform and with a all the browsers that we could get a hold of.... And we cannot reproduce it either... ~ Unsettling

Though i still see plenty (once per hour or so) of exceptions of this type in the logs causing the EAS to fall over:

TypeError: Cannot read properties of undefined (reading 'match')
    at Object.parse (/home/eas/app/node_modules/uri-js/dist/es5/uri.all.js:874:29)
    at OpenIdConnectPlugin.get_authorization_redirect_uri (/home/eas/app/src/plugin/oauth/index.js:2514:27)
    at handle_auth_callback_request (/home/eas/app/src/plugin/oauth/index.js:1649:45)
    at OpenIdConnectPlugin.verify (/home/eas/app/src/plugin/oauth/index.js:1924:29)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async processPipeline (/home/eas/app/src/server.js:399:13)

I can categorize the error into two separate issues:

  1. **decocded state pointer has state_id but state_id does not exist in redis.
    image
  2. decoded state pointer contains the state pointer payload rather than the state_id
    image

How offend it happens can be seen here:
image

Oh actually one or two of the errors in that graph is just deprecation warnings:

[DEP0106] DeprecationWarning: crypto.createDecipher is deprecated.

@Romeren
Copy link
Author

Romeren commented Mar 15, 2023

LOL....!

I found the cause of one of the users problem.....
The user had bookmarked the login page with the state and callback and everything....
No wonder that user was experiencing funky behavior 🤣 😢 🤣

That will most likely explain and solve the second category of issues....

@Romeren
Copy link
Author

Romeren commented Mar 15, 2023

No further action is needed.... i'll wait for you next release.

Sorry for this long thread of silliness.

I'm happy to close the issue at this point.

@travisghansen
Copy link
Owner

Great! That at least explains the issue. No problem on the thread..let's leave it open for now and when I close it down I'll reference any commits that are relevant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants