travisghansen / external-auth-server

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

GitHub oauth #3

Closed runningman84 closed 5 years ago

runningman84 commented 5 years ago

Do you really need a full openid provider or would a GitHub oauth application also work?

travisghansen commented 5 years ago

You need openid currently but I intend to look into pure oauth shortly. Another option would be to run keycloak with GitHub as an identity provider.

I'm working on making the scope of the project a generic external auth service (I haven't committed the changes yet) but with the changes I've made supporting additional services will be much easier and plugin based (I've already implemented ldap in the new code). Given how closely related oauth and openid are I don't think it will require much to get it going.

runningman84 commented 5 years ago

That sounds great. My usecase are just a bunch of home or dev clusters which should not have any stateful software like keycloak...

travisghansen commented 5 years ago

Ok, note that this project stores data in memory or redis so there's a stateful component to even this.

Also note, the authorization code flow (primary focus of this and similar projects) doesn't work so great with SPAs (single page apps) so depending on what you're running it may or may not be useful. Works great for traditional server-side apps though :)

runningman84 commented 5 years ago

Yes but even if you clear the redis data or memory you will just loose the active sessions. In case of keycloak you will loose all users which are stored in postgres.

I just want to secure applications like Prometheus and other internal tools.

travisghansen commented 5 years ago

True. And even if the session goes away typically you would still be authenticated at the provider so it wouldn't require a full login per se.

Prometheus (or presumably grafana) is a SPA and I can try it out for ya to see how well it works. Basically this project would work fine for those use cases as well if you turn off some of the validity checks (ie: expired tokens) and enable cookie expiration.

I can give further explanation if you'd like.

runningman84 commented 5 years ago

What especially would not work with SPA sites?

I think of these applications:

travisghansen commented 5 years ago

Ok, just landed a massive commit which implements pure oauth2.

To answer your question, unless the token/session does not expire it's not great for SPA. In the case of github my testing appears to show the tokens do NOT have an expiration so you should be good generally.

If not, I've now implemented a pipeline process so you could service the request with Basic auth using htpasswd data if you wanted as well. That way any system-to-system tools could also authenticate/access the resources.

travisghansen commented 5 years ago

@runningman84 as an FYI I'm implementing some infrastructure to support fetching user info with provider specific configuration using the oauth2 plugin. When combined with token assertions (not implemented yet) you'll then be able to ensure the target service is only available to you personally and not everyone who can login to github.

runningman84 commented 5 years ago

Additionaly it would be great to have support for GitHub organizations and corresponding teams.

runningman84 commented 5 years ago

This is the corresponding oauth proxy config: https://github.com/bitly/oauth2_proxy/blob/master/README.md#github-auth-provider

travisghansen commented 5 years ago

Done and done.

travisghansen commented 5 years ago

@runningman84 looking for a little feedback here if you think this will help your use-case. Basically my approach with oauth2 is to implement provider specific userinfo plugins (that's what oidc calls it). In the case of github it looks like this in the config token:

...
features: {
  ...
  userinfo: {
    provider: "github",
    config: {
      fetch_teams: true,
      fetch_organizations: true,
      fetch_emails: true
    }
  }
}

Subsequently, in the assertions block you would put something like this:

userinfo: [
  {
    path: "$.login",
    //path: "$.emails[*].email",
    rule: {
      method: "eq",
      value: "travisghansen",

      //method: "regex",
      //value: "/^travis/",// "/pattern/[flags]"

      //method: "in",
      //value: ["travisgh", "travisghansen"],
      //value: ["travisgh", "travisghanse"],

      //method: "contains",
      //value: "travisghansen@yahoo.com2",

      //negate: true,
      //case_insensitive: true
    }
  }
]

Does this make sense? I'm basically taking the approach of let you assert on whatever field(s) you want with various methods (so far eq, regex, in, and contains).

Does it fulfill the need?

runningman84 commented 5 years ago

That sounds good. Can you already give me a full example config how to use traefik with your auth provider and GitHub integration?

travisghansen commented 5 years ago

@runningman84 yeah of course. Can you share the assertion(s) you like to use on the user/orgs/teams? I'll build those in.

runningman84 commented 5 years ago

I have these use cases:

travisghansen commented 5 years ago

@runningman84 got it. Do you want the sessions to ever expire? Independent of that, how frequently would you like the userinfo refreshed to ensure continued validity to the assertions?

runningman84 commented 5 years ago

In a perfect world both values can be configured. Session expire never and refresh one hour sounds like good defaults.

runningman84 commented 5 years ago

Btw. maybe sometimes you want to allow multiple teams or multiple individual users.

travisghansen commented 5 years ago

@runningman84 both are configurable yes. And I do have queued up assertion methods of contains-any and contains-all.

Multiple individual users is already supported. Depending on what you mean by multiple teams it should be covered by one of the above.

travisghansen commented 5 years ago

@runningman84 are you using kubernetes or some other environment? Just getting ready to draft up a doc..

runningman84 commented 5 years ago

I am using kubernetes

travisghansen commented 5 years ago

@runningman84 OK, I've written a howto using github as the example. It should be right in the direction you need. I omitted including custom assertions in the example config_token but they have been documented in the link below. I can help you craft those to the various needs you described above but let's start with basic functionality first and get that up and going. It's very easy to tweak later.

Thanks for the feedback and willingness to try it out!

runningman84 commented 5 years ago

That sounds great I will test that next week and provide you with feedback

runningman84 commented 5 years ago

@travisghansen the auth server seems to work. But I do not really understand how to configure the assertions.

Your default config looks like

        assertions: {
          /**
           * assert the token(s) has not expired
           */
          exp: true

Where do I store the custom config as described here? https://github.com/travisghansen/external-auth-server/blob/master/ASSERTIONS.md

exp: true is not really documented...

Maybe you can improve your example to contain a simple username matching?

travisghansen commented 5 years ago

@runningman84 good timing, I just landed expanded support for assertions (slightly updated syntax).

assertions: {
    ...
    userinfo: [
            {
              query_engine: "jp",
              query: "$.login",
              rule: {
                method: "eq",
                value: "someusername"
                //negate: true,
                //case_insensitive: true
              }
            }
          ]
    ....

exp is asserting the token itself is not expired (ie: basic jwt verification). It has no effect with github from what I can tell as github doesn't expire the tokens.

The documentation definitely needs some help :) Just letting the dust settle a bit before going too wild with it.

travisghansen commented 5 years ago

I forgot to mention, make sure to pull that latest code/image to use the above syntax.

travisghansen commented 5 years ago

I had a little bug in the version I mentioned to pull in the last comment. It's been cleaned up FYI and I'm very close to tagging a release finally.

runningman84 commented 5 years ago

I just changed the config to include the userinfo like this:

        assertions: {
          /**
           * assert the token(s) has not expired
           */
          exp: true,
          userinfo: [
            {
              query_engine: "jp",
              query: "$.login",
              rule: {
                method: "eq",
                value: "runningman84"
                //negate: true,
                //case_insensitive: true
              }
            }
          ]
        },

But now every protected service throws a 500 http error.

Maybe my container is too old?

I also do not understand your helm chart, you are using a imagePullPolicy config but this is commented out in the values.yaml.

travisghansen commented 5 years ago

@runningman84 can you send over the logs to review? It could be an older image yes.

The imagePullPolicy is commented out in values.yaml which basically means, "let kubernetes decide" which pull policy to apply.

Kubernetes' logic is:

So if you deployed with the chart and left that value alone, then simply deleteing the pod(s) currently running should force the latest image to pull when they restart.

runningman84 commented 5 years ago

I just deleted the pod... but the error is still the same.

my logs are quite limited:

$ kubectl logs external-auth-server-5d46fb979-mk622 -n kube-system                                                                                                                  

> external-auth-server@0.1.0 start /app
> node --nouse-idle-notification --expose-gc --max-old-space-size=8192 src/server.js

store options: {"store":"memory","max":0,"ttl":0}
{"service":"external-auth-server","level":"info","message":"starting server on port 8080"}

traefik does not have any log at this timeframe....

travisghansen commented 5 years ago

If the request is erroring 500 you should see some error spit out on the container logs. Unless the traefik server is failing before it ever makes it there.

Can you try to revert the config token back to what it was and tell me if it starts working again?

runningman84 commented 5 years ago

I reverted some sites back to the old config but the error is still there. Does the external auth server provider some debug logs?

travisghansen commented 5 years ago

Can you try with a private/incognito browser? I experienced something similar while developing where all the cookies data combined was quite large (cookie data from other services etc all combined) and it silently failed. Wondering if you've hit the same issue..

Related, I originally designed the server to store all session data (ie: stateless server side) in the cookie but quickly hit browser limits which is why I changed the design to store the sessions server side (redis/memory) and make the cookie simply be a session ID.

runningman84 commented 5 years ago

incognito mode does not change anything :/

travisghansen commented 5 years ago

Must not be that then. If you deployed with the chart then you can set the logLevel value:

# set the logging level
# WARN: debug or above will log secrets
#
# error, warn, info, verbose, debug, silly
logLevel: "info"

I mean, you should see some data logging already if the server is receiving requests etc. Are you seeing anything in the logs at all? Might be good to setup a screenshare/conference to see if that's helpful at all..

Can you send the full response you're getting at the browser?

runningman84 commented 5 years ago

Ok I have fixed the problem. My traefik service got a new loadbalancer ip and my fritzbox was still forwarding the packets to the old ip.

How does the communication work? Does traefik only internally talk to the ingress.kubernetes.io/auth-url? Or is the ingress.kubernetes.io/auth-url also accessed by the client browser?

travisghansen commented 5 years ago

@runningman84 auth-url value should point the to /verify endpoint of the service. It's assumed that endpoint is always being triggered via the reverse proxy itself (ie: sub request of a real request).

For the oauth2 and oidc plugins the service additionally exposes /oauth/callback which is meant to be hit directly by the browser (ie: not a auth request/sub request).

Does that make sense?

travisghansen commented 5 years ago

Did you get the assertions to work?

runningman84 commented 5 years ago

I guess yes but you can do the final test. Just check your mails.

runningman84 commented 5 years ago

Do you have an example for these use cases?

travisghansen commented 5 years ago

@runningman84 this is a bit nuanced depending on what you really want. But here are a couple examples using team/org IDs (I'd recommend that over names). For your first use-case there are a couple ways:

  {
    query: "$.organizations[*].id",
    rule: {
      method: "contains",
      value: 1

      //negate: true,
      //case_insensitive: true
    }
  }

For the second use-case (I'm assuming team IDs are globally unique):

  {
    query_engine: "jp",
    query: "$.teams[*].id",
    rule: {
      method: "contains",
      value: 1

      //negate: true,
      //case_insensitive: true
    }
  }

If you want to allow to be part of a list of teams/orgs change the method to contains-any and set the value to an array of values like value: [1, 3, 9].

If fetching organizations, teams and emails is turned on the data for each is added to the userinfo data with the respective names/keys:

userinfo.organizations = [...]
userinfo.teams = [...]
userinfo.emails = [...]

Lastly, remember that all the assertions added are LOGICAL AND, meaning ALL of them must pass assertion or the result is a failure.

travisghansen commented 5 years ago

Any luck with these?

runningman84 commented 5 years ago

I suppose they will work but I cannot really test it because the browser complains about a too long url.

travisghansen commented 5 years ago

Can you give me more detail? Something I can look into?

runningman84 commented 5 years ago

Github throws this error:

414 Request-URI Too Large
nginx

The url looks like this: https://github.com/login/oauth/authorize?response_type=code&client_id=07fcc7b7c3696fee3c8a&redirect_uri=https%3A%2F%2Feas.example.com%2Foauth%2Fcallback%3F__eas_oauth_handler__%3Dauthorization_callback&scope=user&state=273377c96c3e52fe3f4ad.......................

travisghansen commented 5 years ago

That's pretty strange by itself. Even more so that adding assertions would impact it at all.

Do you only get that with the added assertions or does it do that generally to you now?

runningman84 commented 5 years ago

I haven't had time to reproduce it myself. A team member tried it. Do you have an ETA for the server side configuration?

travisghansen commented 5 years ago

It's the next big item for me. Wrapping up some header work and then on to that.

Mostly struggling how to configure that and keep it flexible. Store them in redis? SQL based storage? Or something else altogether...like hitting some other url and let it be completely managed externally? Any feedback is welcome on that front :)

runningman84 commented 5 years ago

I would like to store the config in a configmap using helm. I would rather have the auth server without any persistent storage. Imagine there is some kind of storage problem and you cannot access some admin services because their auth relies on storage too.

travisghansen commented 5 years ago

Wise words!