travisghansen / external-auth-server

easy auth for reverse proxies
MIT License
330 stars 44 forks source link

Support introspection of access token in jwt plugin #98

Closed Electrofenster closed 3 years ago

Electrofenster commented 3 years ago

I think it would be very grateful if the jwt plugin supports the introspection of access tokens to verify that the access token is still valid and authenticated via the introspection endpoint of the oidc provider.

I think it could be configureable as in the oidc plugin, so anyone who need this can activate it:

features: {
  /**
  * check token validity with provider during assertion process
  */
  introspect_access_token: true,

  /**
   * if introspect_access_token is true, how long in seconds to cache the result
   * if not a number greater than 0, the introspection endpoint will be requested *every* verify request
   * NOTE: the cache is stored on a per-eas-session basis vs a per-token (jti) basis
   */
  introspect_expiry: 0,
},
Electrofenster commented 3 years ago

In the meantime I created a second little middleware (https://github.com/Electrofenster/jwt-verifier) which uses the access-token from yours and validate it with the introspection endpoint on each request and also my middleware adds the x-userinfo-header as I suggestet it in #99 when accessing this service which uses your and mine middleware.

travisghansen commented 3 years ago

I should be able to add both...been a bit of a crazy week but when I get a little time I’m sure both requests can get knocked out.

Electrofenster commented 3 years ago

@travisghansen any news on this? :)

travisghansen commented 3 years ago

Not yet but I’m getting close to putting it together! It’s a relatively easy change.

Maybe I’ll get this pushed out real quick for ya in the next branch as I have some other additions that will take a little more time for my next release.

travisghansen commented 3 years ago

I'm looking into both your requests. Out of curiosity are you issuing an access token from keycloak directly? Or are you getting from eas after it does the authorization code flow and then using the token in some backend server-to-server process after parsing it from a header? I think I understand what you're doing here but want to make sure I understand.

Also, if you are indeed issuing an access_token direct from keycloak what the easiest way to do that not going through the authorization code flow (ie: client direct to keycloak)?

Electrofenster commented 3 years ago

In this case I got the access_token from eas. It would be very usefull if the OIDC plugin calls the introspection endpoint on every request to verify the user session ist still valid or to make sure that the user is still enabled or still exists. You can pass the accessToken to the introspection function of your client like this: client.introspect(accessToken)

Also on this case, I'll use the userinfo-header to verify my logged in user on the backend.

And on your last question. Yes, I think this should be the easiest way. Below is a cURL example:

curl --request POST \
  --url http://KEYCLOAK.tld/auth/realms/REALM/protocol/openid-connect/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data client_id=CLIENT_ID \
  --data client_secret=CLIENT_SECRET \
  --data username=USERNAME \
  --data password=PASSWORD \
  --data grant_type=password
travisghansen commented 3 years ago

I must have some configuration off. It seems every time I introspect the token in this context it comes back as false. Any ideas what configuration option I may have off?

I've pretty well got this buttoned up regardless. Should have something committed within the next day or so.

Electrofenster commented 3 years ago

haha, the same problem I've got on my simple plugin as I developed it! :D But I don't remember what it was :/

The one thing is, that the access_tokens is only 5 minutes valid with default settings (in keycloak). Maybe you can test it with an the cURL from below to test the access_token on the introspection endpoint right after you got it. I think, you should get the introspection response.

I just think you should test it like this:

curl --request POST \
  --url http://KEYCLOAK.tld/auth/realms/REALM/protocol/openid-connect/token/introspect \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data client_id=CLIENT_ID \
  --data client_secret=CLIENT_SECRET \
  --data token=ACCESS_TOKEN

when you get also

{ "active": false }

i think you keycloak is misconfigured

when you got the right response from introspection endpoint your access_token/session from eas is invalid. You can see it in keycloak under users -> your user -> sessions and check if there is the session from your client which you configured for eas to login to keycloak.

travisghansen commented 3 years ago

I figured it out..it was actually some bad code. This is really close to landing in next branch to have you try it out.

travisghansen commented 3 years ago

Please see the doc here: https://github.com/travisghansen/external-auth-server/blob/next/PLUGINS.md#jwt

Update your image tags to next and create a new config_token with the appropriate settings. Let me know how it goes!

Electrofenster commented 3 years ago

Well! Now it works! Thanks!

But I've one more little thing :D Could you add a setting to the jwt plugin to return a "401 Unauthorized" when the access_token is not valid? The redirect to the login could be very hard to figure out in a app when calling the expected endpoint.

travisghansen commented 3 years ago

Ah I should audit that a bit more closely. What was the exact error response/scenario that you got where you are expecting the 401 vs the 403?

UPDATE: doing a quick audit I think the only change that should be made is return a 401 when the token introspection check fails. The other scenarios should remain a 403.

Another change I could make is to auto detect the realm when oidc is enabled and set it to the token authentication endpoint of the issuer.

Thoughts?

travisghansen commented 3 years ago

New images are built in next and ready for testing. It includes a proper return code for introspection failures and better logic around the realm returned when oidc is enabled.

On a semi-related note, I'm getting close to implementing the oidc back-channel logout functionality. Any interest in helping test that and provide design input? Given the stateless design of eas it provides a unique set of challenges associate with back-channel logout but I think I've got a solution that should do the trick and I'm ready to start implementation.

Electrofenster commented 3 years ago

Yeah, I think it's a very good idea to additionally return a 401 when the token introspection check fails. (Usefull when using your plugin in an API as middleware) when accessing this with a browser I think the redirect to keycloak is more usefull.

I don't know why it shouldn't be usefull to auto detect the realm.

I tested your new image in my setup and it works very good.

Sure! Let me know when you need some help testing the back-channel logout functionality or when you may need some design input.

travisghansen commented 3 years ago

I’ve got several additions going on with logout scenarios currently:

The first 2 are done, now I just need to implement the 3rd and then it should be ready for testing.

Electrofenster commented 3 years ago

The introspection of access_tokens directly from keycloak don't work or did I have a mistake? :)

travisghansen commented 3 years ago

I don’t understand the comment :(

Electrofenster commented 3 years ago

if you got the access_token directly from keycloak e.g. with the cURL from above, does the jwt-plugin introspect the access_token on every request (when enabled)?

travisghansen commented 3 years ago

Oh, yeah it will introspect every request (unless introspection cache is turned in then whenever appropriate).

Electrofenster commented 3 years ago

One question to the 401

  1. setup eas with oidc + jwt plugin
  2. login through keycloak
  3. get access_token
  4. log user in keycloak out (admin -> users -> your_user -> sessions -> logout from all)
  5. make request with cURL and the access_token from step 3

As you use a invalid access_token you'll got a 201 with an redirect to keycloak login. Well that's not a good point when using the jwt-plugin in an API. Could you integrate a possible option to get a 401 unauthrorized instead. Currently I check if I could parse the HTML with JSON and when there is an error I require a new access_token with the refresh_token. That's not very good workflow. It's much easier when you can set an option to get a 401 unauthorized and refresh then the access_token.

travisghansen commented 3 years ago

I'm not sure I follow your workflow. The jwt plugin never sends a 201 or any redirect...

Electrofenster commented 3 years ago

I thought it was an 201 but it is 200 as status code, sorry. But I got as response any html.. I'll get this in my app:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" class="login-pf">
<head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="robots" content="noindex, nofollow">
            <meta name="viewport" content="width=device-width,initial-scale=1"/>
    <title>Sign in to game</title>
    <link rel="icon" href="/auth/resources/zejp1/login/keycloak/img/favicon.ico" />
            <link href="/auth/resources/zejp1/common/keycloak/web_modules/@fortawesome/fontawesome-free/css/icons/all.css" rel="stylesheet" />
            <link href="/auth/resources/zejp1/common/keycloak/web_modules/@patternfly/react-core/dist/styles/base.css" rel="stylesheet" />
            <link href="/auth/resources/zejp1/common/keycloak/web_modules/@patternfly/react-core/dist/styles/app.css" rel="stylesheet" />
            <link href="/auth/resources/zejp1/common/key

also as header I'll get something like this:

{connection: close, set-cookie: AUTH_SESSION_ID=9e6146bb-18ca-41cc-b547-a306f690d2f4.keycloak-production-app; Version=1; Path=/auth/realms/game/; SameSite=None; Secure; HttpOnly,AUTH_SESSION_ID_LEGACY=9e6146bb-18ca-41cc-b547-a306f690d2f4.keycloak-production-app; Version=1; Path=/auth/realms/game/; Secure; HttpOnly,KC_RESTART=ey[...]

also when using cURL make sure you add the -L flag to follow redirects:

curl -k --request GET \
  --url https://api.local/user/me \
  --header 'Authorization: Bearer ACCESS_TOKEN' -L

then I'll get the same html from above. ;)

or use any invalid ACCESS_TOKEN like this:

curl -k --request GET \
  --url https://api.local/user/me \
  --header 'Authorization: Bearer INVALID_ACCESS_TOKEN' -L
travisghansen commented 3 years ago

I think some wires are getting crossed somewhere. It appears you have the service protected by both the jwt and the oidc plugins? Maybe send over a little diagram showing what services/clients/etc you have so I can better understand how you're getting that response.

If the service is solely authenticated with the jwt plugin and the introspection fails you should never get a 200 or any html. Not only that, eas never returns html...ever. So something is off somewhere.

Electrofenster commented 3 years ago

LOL my mistake xD

I used both oidc and jwt :/ With only the jwt 'll get the 401

travisghansen commented 3 years ago

So all good now? Anything outstanding issues?

Electrofenster commented 3 years ago

For now anything is working great!

travisghansen commented 3 years ago

Awesome! Thanks for the suggestion! It was a good one :)

All the new logout functionality has landed as well if you care to try out that. I haven't committed the updated doc just yet going into details though.

Electrofenster commented 3 years ago

Just ping me when the docs are updated so I can test it. :)